@xenon-device-management/xenon 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +57 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
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 fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
23
|
+
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
24
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
25
|
+
const UserService_1 = require("../../src/services/UserService");
|
|
26
|
+
const config_1 = require("../../src/config");
|
|
27
|
+
const prisma_1 = require("../../src/prisma");
|
|
28
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
29
|
+
const sessionCredentials_1 = require("../../src/services/session/sessionCredentials");
|
|
30
|
+
/**
|
|
31
|
+
* Session create judges the credentials it is given by REST's rule: a key
|
|
32
|
+
* pair or session token says who the caller is only while that user exists
|
|
33
|
+
* and is ACTIVE. Through 2.14 the pair was checked against the key alone
|
|
34
|
+
* (ApiKeyService.verifyPair) and the session token against its signature
|
|
35
|
+
* alone, so a user set to Inactive, or deleted, kept creating sessions as
|
|
36
|
+
* themselves, past XENON_REQUIRE_SESSION_TOKEN too.
|
|
37
|
+
*
|
|
38
|
+
* Such credentials now count as wrong ones: with the gate on the session is
|
|
39
|
+
* refused, with it off it runs with no owner, like any session presenting
|
|
40
|
+
* credentials that don't check out.
|
|
41
|
+
*
|
|
42
|
+
* A session token also has to carry the `sessions` scope, as a key pair
|
|
43
|
+
* must: POST /auth/token gave one to any credential, a read-only one
|
|
44
|
+
* included.
|
|
45
|
+
*/
|
|
46
|
+
// authorizeSessionRequest is private: the single place a create's identity is
|
|
47
|
+
// decided. It is handed the credentials createSession took out of the caps.
|
|
48
|
+
const invoke = (svc, caps) => svc.authorizeSessionRequest(caps, (0, sessionCredentials_1.takeSessionCredentials)(caps));
|
|
49
|
+
const capsWith = (options) => ({
|
|
50
|
+
alwaysMatch: { 'xe:options': options },
|
|
51
|
+
firstMatch: [{}],
|
|
52
|
+
});
|
|
53
|
+
function refusalOf(promise) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _a;
|
|
56
|
+
try {
|
|
57
|
+
yield promise;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
describe('session create: the credentials of an inactive or deleted user', () => {
|
|
66
|
+
let svc;
|
|
67
|
+
let dir;
|
|
68
|
+
let keys;
|
|
69
|
+
let restore;
|
|
70
|
+
let authDisabledBefore;
|
|
71
|
+
let gateBefore;
|
|
72
|
+
let users;
|
|
73
|
+
let findById;
|
|
74
|
+
const keyRow = { id: 'key_alice', userId: 'usr_alice', scopes: 'sessions', teamId: null };
|
|
75
|
+
const alice = () => users.usr_alice;
|
|
76
|
+
const pair = () => capsWith({ accessKey: 'xen_alice', token: 'tk' });
|
|
77
|
+
const token = (claims = { sub: 'usr_alice', scopes: 'sessions' }) => keys.sign(claims, { audience: 'xenon-session', ttlSeconds: 600 });
|
|
78
|
+
const gate = (on) => {
|
|
79
|
+
if (on)
|
|
80
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = 'true';
|
|
81
|
+
else
|
|
82
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
83
|
+
};
|
|
84
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
86
|
+
config_1.config.authDisabled = false;
|
|
87
|
+
gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
88
|
+
restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
|
|
89
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-create-owner-'));
|
|
90
|
+
keys = new JwtKeyService_1.JwtKeyService();
|
|
91
|
+
yield keys.init(dir);
|
|
92
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
|
|
93
|
+
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
94
|
+
verifyPair: sinon_1.default.stub().resolves(keyRow),
|
|
95
|
+
hasScope: (row, req) => {
|
|
96
|
+
const owned = row.scopes.split(',');
|
|
97
|
+
return owned.includes('admin') || req.some((s) => owned.includes(s));
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
users = { usr_alice: { id: 'usr_alice', role: 'MEMBER', status: 'ACTIVE' } };
|
|
101
|
+
findById = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = users[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
102
|
+
typedi_1.Container.set(UserService_1.UserService, { findById });
|
|
103
|
+
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
104
|
+
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
105
|
+
}));
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
108
|
+
if (gateBefore === undefined)
|
|
109
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
110
|
+
else
|
|
111
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
|
|
112
|
+
sinon_1.default.restore();
|
|
113
|
+
restore();
|
|
114
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
|
|
117
|
+
beforeEach(() => gate(true));
|
|
118
|
+
it("refuses an Inactive user's access key and token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
alice().status = 'INACTIVE';
|
|
120
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
|
|
121
|
+
}));
|
|
122
|
+
it("refuses a deleted user's key, whose row outlived them", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
users.usr_alice = undefined;
|
|
124
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
|
|
125
|
+
}));
|
|
126
|
+
it("refuses an Inactive user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
|
+
alice().status = 'INACTIVE';
|
|
128
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
129
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
|
|
130
|
+
}));
|
|
131
|
+
it("refuses a deleted user's session token, which is still within its lifetime", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
users.usr_alice = undefined;
|
|
133
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
134
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
|
|
135
|
+
}));
|
|
136
|
+
it("still admits an Active user's key and token, and their session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
const byPair = yield invoke(svc, pair());
|
|
138
|
+
(0, chai_1.expect)(byPair).to.include({ apiKeyId: 'key_alice', userId: 'usr_alice' });
|
|
139
|
+
const byToken = yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
140
|
+
(0, chai_1.expect)(byToken).to.include({ apiKeyId: null, userId: 'usr_alice' });
|
|
141
|
+
}));
|
|
142
|
+
it('looks the owner up once, though the gate and attribution both verify the token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
|
+
yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
144
|
+
(0, chai_1.expect)(findById.callCount).to.equal(1);
|
|
145
|
+
}));
|
|
146
|
+
});
|
|
147
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN off', () => {
|
|
148
|
+
beforeEach(() => gate(false));
|
|
149
|
+
it("gives no owner to a session made with an Inactive user's key", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
alice().status = 'INACTIVE';
|
|
151
|
+
const res = yield invoke(svc, pair());
|
|
152
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
153
|
+
}));
|
|
154
|
+
it("gives no owner to a session made with a deleted user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
users.usr_alice = undefined;
|
|
156
|
+
const res = yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
157
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
158
|
+
}));
|
|
159
|
+
it('looks an Active owner up once for a key and token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
const res = yield invoke(svc, pair());
|
|
161
|
+
yield res.leaseAccess();
|
|
162
|
+
(0, chai_1.expect)(res.userId).to.equal('usr_alice');
|
|
163
|
+
(0, chai_1.expect)(findById.callCount).to.equal(1);
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
describe('a session token needs the `sessions` scope', () => {
|
|
167
|
+
for (const on of [false, true]) {
|
|
168
|
+
describe(`(gate ${on ? 'on' : 'off'})`, () => {
|
|
169
|
+
beforeEach(() => gate(on));
|
|
170
|
+
it('refuses one minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
const caps = capsWith({
|
|
172
|
+
sessionToken: yield token({ sub: 'usr_alice', scopes: 'devices' }),
|
|
173
|
+
});
|
|
174
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/sessions` scope/);
|
|
175
|
+
}));
|
|
176
|
+
it('refuses one minted before session tokens carried their scopes, saying so', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
177
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice' }) });
|
|
178
|
+
const refusal = yield refusalOf(invoke(svc, caps));
|
|
179
|
+
(0, chai_1.expect)(refusal).to.match(/carries no `sessions` scope/);
|
|
180
|
+
(0, chai_1.expect)(refusal).to.match(/minted by Xenon 2\.14 or earlier carry none/);
|
|
181
|
+
(0, chai_1.expect)(refusal).to.match(/mint a new one/);
|
|
182
|
+
}));
|
|
183
|
+
it('admits one with `sessions` or `admin`', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
for (const scopes of ['sessions', 'admin', 'devices,sessions']) {
|
|
185
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
|
|
186
|
+
(0, chai_1.expect)((yield invoke(svc, caps)).userId).to.equal('usr_alice');
|
|
187
|
+
}
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Taking over a lease someone else created is an ownership override: a
|
|
193
|
+
// SUPER_ADMIN, or a credential with the `admin` scope. A session token was
|
|
194
|
+
// judged by its user's role alone, so an ADMIN's key without the admin
|
|
195
|
+
// scope, which may not override, minted a session token that could.
|
|
196
|
+
describe('a session token overrides a lease only as the credential that minted it could', () => {
|
|
197
|
+
beforeEach(() => gate(false));
|
|
198
|
+
const overrideWith = (role, scopes) => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
alice().role = role;
|
|
200
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
|
|
201
|
+
return (yield (yield invoke(svc, caps)).leaseAccess()).canOverride;
|
|
202
|
+
});
|
|
203
|
+
it("is refused for an ADMIN's token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
204
|
+
(0, chai_1.expect)(yield overrideWith('ADMIN', 'sessions')).to.equal(false);
|
|
205
|
+
}));
|
|
206
|
+
it("is allowed for an ADMIN's or SUPER_ADMIN's token minted with the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
207
|
+
(0, chai_1.expect)(yield overrideWith('ADMIN', 'admin,sessions')).to.equal(true);
|
|
208
|
+
(0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'admin,sessions')).to.equal(true);
|
|
209
|
+
}));
|
|
210
|
+
// As the SUPER_ADMIN's key pair and Bearer token override.
|
|
211
|
+
it("is allowed for a SUPER_ADMIN's token minted without it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
+
(0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'sessions')).to.equal(true);
|
|
213
|
+
}));
|
|
214
|
+
// The scope is fixed when the token is minted; the role is read now. An
|
|
215
|
+
// admin demoted after minting overrides no more.
|
|
216
|
+
it('is refused once the admin who minted it is a member', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
(0, chai_1.expect)(yield overrideWith('MEMBER', 'admin,sessions')).to.equal(false);
|
|
218
|
+
}));
|
|
219
|
+
it("is refused for a member's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
220
|
+
(0, chai_1.expect)(yield overrideWith('MEMBER', 'sessions')).to.equal(false);
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
// The owner lookup that fails is not a token that doesn't check out: the
|
|
224
|
+
// create is refused, as for a key whose owner can't be looked up. It ran
|
|
225
|
+
// with no owner (gate off) or was refused as "invalid or expired" (gate on).
|
|
226
|
+
describe('a session token whose owner cannot be looked up', () => {
|
|
227
|
+
for (const on of [false, true]) {
|
|
228
|
+
it(`refuses the create (gate ${on ? 'on' : 'off'})`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
229
|
+
gate(on);
|
|
230
|
+
findById.rejects(new Error('db down'));
|
|
231
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
232
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.equal('db down');
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
it('still counts a token that does not check out as none (gate off)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
236
|
+
gate(false);
|
|
237
|
+
const res = yield invoke(svc, capsWith({ sessionToken: 'not-a-jwt' }));
|
|
238
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
239
|
+
(0, chai_1.expect)(findById.called).to.equal(false);
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -146,7 +146,7 @@ describe('createSession — credentials never reach the driver or storage', () =
|
|
|
146
146
|
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
147
|
if (!t.startsWith('jwt:'))
|
|
148
148
|
throw new Error('bad token');
|
|
149
|
-
return { sub: t.slice('jwt:'.length), teamId: null };
|
|
149
|
+
return { sub: t.slice('jwt:'.length), teamId: null, scopes: 'sessions' };
|
|
150
150
|
}),
|
|
151
151
|
});
|
|
152
152
|
// The owner's teams: none, and not read from the developer's database.
|
|
@@ -0,0 +1,180 @@
|
|
|
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 supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
19
|
+
const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
|
|
20
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
21
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
22
|
+
// The driver's own log class (test/helpers/driver-log.ts): an iPhone's and a
|
|
23
|
+
// simulator's `logs.syslog` extend it, so what Xenon reads is what the driver keeps.
|
|
24
|
+
const driver_log_1 = require("../helpers/driver-log");
|
|
25
|
+
// How the Device logs tab reads an iPhone line's level (web log-derive.ts).
|
|
26
|
+
const DASHBOARD_SYSLOG = /<(Error|Fault|Warning)>/;
|
|
27
|
+
class TestDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
28
|
+
context() {
|
|
29
|
+
return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4723, nodeId: 'this-server' };
|
|
30
|
+
}
|
|
31
|
+
flushIntervalMs() {
|
|
32
|
+
return 60000;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const IPHONE = {
|
|
36
|
+
udid: '00008110-000A',
|
|
37
|
+
platform: 'ios',
|
|
38
|
+
realDevice: true,
|
|
39
|
+
host: 'http://127.0.0.1:4723',
|
|
40
|
+
nodeId: 'this-server',
|
|
41
|
+
};
|
|
42
|
+
const SIMULATOR = Object.assign(Object.assign({}, IPHONE), { udid: 'SIM-1', realDevice: false });
|
|
43
|
+
const syslog = (n, level = 'Notice') => `Oct 5 20:31:${String(n % 60).padStart(2, '0')} iPhone Shop[4127] <${level}>: line ${n}`;
|
|
44
|
+
function deviceLogs(sessionId) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const res = yield (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(selector_health_fixture_1.ADMIN)).get(`/session/${sessionId}/logs/device`);
|
|
47
|
+
(0, chai_1.expect)(res.status, res.text).to.equal(200);
|
|
48
|
+
return res.body;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
describe("An iPhone or simulator session's Device logs", function () {
|
|
52
|
+
this.timeout(60000);
|
|
53
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
54
|
+
let logs;
|
|
55
|
+
let sessionId;
|
|
56
|
+
let driverLog;
|
|
57
|
+
let count = 0;
|
|
58
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
count += 1;
|
|
60
|
+
sessionId = `dl-ios-${count}`;
|
|
61
|
+
yield scratch.db.session.create({
|
|
62
|
+
data: {
|
|
63
|
+
id: sessionId,
|
|
64
|
+
device_udid: IPHONE.udid,
|
|
65
|
+
device_platform: 'ios',
|
|
66
|
+
device_version: '26.0',
|
|
67
|
+
desired_capabilities: '{}',
|
|
68
|
+
session_capabilities: '{}',
|
|
69
|
+
node_id: 'this-server',
|
|
70
|
+
has_live_video: false,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
logs = new TestDeviceLogs();
|
|
74
|
+
driverLog = new driver_log_1.TestDriverLog();
|
|
75
|
+
}));
|
|
76
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
yield logs.stop(sessionId);
|
|
78
|
+
}));
|
|
79
|
+
it('keeps the lines from the create and from the run, each once, in order', () => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
// Logged during the create, before Xenon could listen: in the driver's buffer.
|
|
81
|
+
for (let i = 0; i < 3; i++)
|
|
82
|
+
driverLog.line(syslog(i));
|
|
83
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
84
|
+
for (let i = 3; i < 250; i++)
|
|
85
|
+
driverLog.line(syslog(i));
|
|
86
|
+
yield logs.stop(sessionId);
|
|
87
|
+
(0, chai_1.expect)(driverLog.listenerCount('output')).to.equal(0);
|
|
88
|
+
driverLog.line(syslog(999)); // after the session: not its line
|
|
89
|
+
const rows = yield deviceLogs(sessionId);
|
|
90
|
+
(0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal(Array.from({ length: 250 }, (_, i) => syslog(i)));
|
|
91
|
+
}));
|
|
92
|
+
it("leaves the driver's lines to the test that asks for them", () => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
driverLog.line(syslog(1));
|
|
94
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
95
|
+
driverLog.line(syslog(2));
|
|
96
|
+
yield logs.stop(sessionId);
|
|
97
|
+
const forTheTest = yield driverLog.getLogs();
|
|
98
|
+
(0, chai_1.expect)(forTheTest.map((e) => e.message)).to.deep.equal([
|
|
99
|
+
syslog(1),
|
|
100
|
+
syslog(2),
|
|
101
|
+
]);
|
|
102
|
+
}));
|
|
103
|
+
it("keeps each line's text, so Errors only finds the phone's errors, at the time it arrived", () => __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
105
|
+
const before = Date.now();
|
|
106
|
+
driverLog.line(syslog(1));
|
|
107
|
+
driverLog.line(syslog(2, 'Error'));
|
|
108
|
+
driverLog.line(syslog(3, 'Fault'));
|
|
109
|
+
const after = Date.now();
|
|
110
|
+
yield logs.stop(sessionId);
|
|
111
|
+
const rows = yield deviceLogs(sessionId);
|
|
112
|
+
(0, chai_1.expect)(rows.map((r) => { var _a, _b; return (_b = (_a = DASHBOARD_SYSLOG.exec(r.message)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; })).to.deep.equal([
|
|
113
|
+
null,
|
|
114
|
+
'Error',
|
|
115
|
+
'Fault',
|
|
116
|
+
]);
|
|
117
|
+
for (const r of rows) {
|
|
118
|
+
const t = new Date(r.timestamp).getTime();
|
|
119
|
+
(0, chai_1.expect)(t).to.be.within(before, after);
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
it("records a simulator's compact lines, without `log stream`'s banner", () => __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
driverLog.line('Filtering the log data using "subsystem != \\"com.apple.CoreTelephony\\""');
|
|
124
|
+
driverLog.line('Timestamp Ty Process[PID:TID]');
|
|
125
|
+
yield logs.start({ sessionId, device: SIMULATOR, since: Date.now() - 60000, driverLog });
|
|
126
|
+
driverLog.line('2026-10-05 20:31:40.123 F Shop[4127:8812] Terminating app');
|
|
127
|
+
driverLog.line('\tThread 0 Crashed:');
|
|
128
|
+
yield logs.stop(sessionId);
|
|
129
|
+
(0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
|
|
130
|
+
'2026-10-05 20:31:40.123 F Shop[4127:8812] Terminating app',
|
|
131
|
+
'\tThread 0 Crashed:',
|
|
132
|
+
]);
|
|
133
|
+
}));
|
|
134
|
+
it("keeps the app's own lines, the errors inside it, the app manager's lines and faults, and says so first", () => __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
const line = (proc, text, level = 'Notice') => `Oct 5 23:48:33 Rabindras-iPhone ${proc} <${level}>: ${text}`;
|
|
136
|
+
// From the create, before Xenon listens: the app's first line comes
|
|
137
|
+
// before the phone announces its pid.
|
|
138
|
+
driverLog.line(line('Shop[4127]', 'first line'));
|
|
139
|
+
driverLog.line(line('runningboardd(RunningBoard)[34]', 'Acquiring [app<com.example.shop(UUID)>:4127]'));
|
|
140
|
+
yield logs.start({
|
|
141
|
+
sessionId,
|
|
142
|
+
device: IPHONE,
|
|
143
|
+
since: Date.now() - 60000,
|
|
144
|
+
driverLog,
|
|
145
|
+
appUnderTest: 'com.example.shop',
|
|
146
|
+
});
|
|
147
|
+
driverLog.line(line('duetexpertd[412]', 'chatter'));
|
|
148
|
+
driverLog.line(line('Shop(Networking)[4127]', 'request failed', 'Error'));
|
|
149
|
+
driverLog.line(line('wifid[61]', 'link lost', 'Fault'));
|
|
150
|
+
yield logs.stop(sessionId);
|
|
151
|
+
(0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
|
|
152
|
+
"Xenon: Kept here: what the app under test (com.example.shop) logs itself, every error inside it, what the phone says about its launch, state, crashes and end, and faults. The rest of the phone's log is left out.",
|
|
153
|
+
line('Shop[4127]', 'first line'),
|
|
154
|
+
line('runningboardd(RunningBoard)[34]', 'Acquiring [app<com.example.shop(UUID)>:4127]'),
|
|
155
|
+
line('Shop(Networking)[4127]', 'request failed', 'Error'),
|
|
156
|
+
line('wifid[61]', 'link lost', 'Fault'),
|
|
157
|
+
]);
|
|
158
|
+
}));
|
|
159
|
+
it('records nothing for a session that skips log capture', () => __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now(), driverLog: undefined });
|
|
161
|
+
(0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(false);
|
|
162
|
+
(0, chai_1.expect)(yield deviceLogs(sessionId)).to.deep.equal([]);
|
|
163
|
+
}));
|
|
164
|
+
it(`holds to the same limit as Android, ${deviceLogBook_1.DEVICE_LOG_LINE_LIMIT} lines then errors`, () => __awaiter(this, void 0, void 0, function* () {
|
|
165
|
+
yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
|
|
166
|
+
for (let i = 0; i < deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 100; i++) {
|
|
167
|
+
driverLog.line(syslog(i, i >= deviceLogBook_1.DEVICE_LOG_LINE_LIMIT && i % 2 === 0 ? 'Error' : 'Notice'));
|
|
168
|
+
}
|
|
169
|
+
yield logs.stop(sessionId);
|
|
170
|
+
const rows = yield deviceLogs(sessionId);
|
|
171
|
+
(0, chai_1.expect)(rows).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1 + 50 + 1);
|
|
172
|
+
(0, chai_1.expect)(rows[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT].message).to.match(/^Xenon: This session reached 10,000/);
|
|
173
|
+
(0, chai_1.expect)(rows[rows.length - 1].message).to.match(/^Xenon: 50 device log lines were left out/);
|
|
174
|
+
}));
|
|
175
|
+
it("doesn't record a node's iPhone, whose driver is the node's", () => {
|
|
176
|
+
(0, chai_1.expect)(logs.appliesTo(IPHONE)).to.equal(true);
|
|
177
|
+
(0, chai_1.expect)(logs.appliesTo(SIMULATOR)).to.equal(true);
|
|
178
|
+
(0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, IPHONE), { nodeId: 'a-node' }))).to.equal(false);
|
|
179
|
+
});
|
|
180
|
+
});
|