@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,158 @@
|
|
|
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 DeviceService = __importStar(require("../../src/data-service/device-service"));
|
|
53
|
+
const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
|
|
54
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
55
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
56
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
57
|
+
const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
|
|
58
|
+
const VideoPipelineService_1 = require("../../src/services/VideoPipelineService");
|
|
59
|
+
const LocalSession_1 = require("../../src/sessions/LocalSession");
|
|
60
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
61
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
62
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
63
|
+
/**
|
|
64
|
+
* What Xenon does around a session on an iOS simulator, whose WebDriverAgent
|
|
65
|
+
* the XCUITest driver runs on this Mac.
|
|
66
|
+
*
|
|
67
|
+
* A session that passed every command ended failed: its end stopped a
|
|
68
|
+
* performance recording that Xenon starts only on an iPhone, the driver
|
|
69
|
+
* refuses that on a simulator without relaxed security, and the refused stop
|
|
70
|
+
* counted as a failed command of the session. Its video was never written:
|
|
71
|
+
* it was read from the iPhone stream's USB forward (iproxy), which on a
|
|
72
|
+
* simulator forwards nothing, while the simulator's own WebDriverAgent serves
|
|
73
|
+
* the picture on this Mac at the session's mjpegServerPort.
|
|
74
|
+
*/
|
|
75
|
+
const SIMULATOR = '35429994-B029-4AF4-9634-880B89DAC782';
|
|
76
|
+
const IPHONE = '00008150-000E78612168C01C';
|
|
77
|
+
const device = (udid, realDevice) => ({
|
|
78
|
+
udid,
|
|
79
|
+
name: 'iPhone 16 Pro',
|
|
80
|
+
platform: 'ios',
|
|
81
|
+
sdk: '18.4',
|
|
82
|
+
host: 'http://127.0.0.1:4723',
|
|
83
|
+
realDevice,
|
|
84
|
+
deviceType: realDevice ? 'real' : 'simulator',
|
|
85
|
+
screenWidth: '402',
|
|
86
|
+
screenHeight: '874',
|
|
87
|
+
});
|
|
88
|
+
describe('iOS simulator sessions: around the session', function () {
|
|
89
|
+
this.timeout(30000);
|
|
90
|
+
afterEach(() => sinon_1.default.restore());
|
|
91
|
+
describe('the end of a session', () => {
|
|
92
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
93
|
+
let restoreRegs;
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
|
|
96
|
+
typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession: sinon_1.default.stub().resolves() });
|
|
97
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
|
|
98
|
+
sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
|
|
99
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
100
|
+
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'removeSession');
|
|
101
|
+
});
|
|
102
|
+
afterEach(() => restoreRegs());
|
|
103
|
+
/** Deletes a session on `phone`; resolves to whether its end stopped a performance recording. */
|
|
104
|
+
function endSessionOn(phone) {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
const stopPerformanceRecording = sinon_1.default.stub().resolves(null);
|
|
107
|
+
const session = {
|
|
108
|
+
isStopping: false,
|
|
109
|
+
getId: () => 's-1',
|
|
110
|
+
getDevice: () => phone,
|
|
111
|
+
stopPerformanceRecording,
|
|
112
|
+
isVideoRecordingInProgress: () => false,
|
|
113
|
+
};
|
|
114
|
+
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(session);
|
|
115
|
+
yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(sinon_1.default.stub().resolves(), 's-1');
|
|
116
|
+
return stopPerformanceRecording.called;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
it('stops no performance recording on a simulator, where none was started', () => __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
(0, chai_1.expect)(yield endSessionOn(device(SIMULATOR, false))).to.equal(false);
|
|
121
|
+
}));
|
|
122
|
+
it("still stops an iPhone's, which Xenon started", () => __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
(0, chai_1.expect)(yield endSessionOn(device(IPHONE, true))).to.equal(true);
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
describe('the session video', () => {
|
|
127
|
+
let startRecording;
|
|
128
|
+
let startStream;
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
startRecording = sinon_1.default.stub(VideoPipelineService_1.VideoPipelineService.prototype, 'startRecording').resolves();
|
|
131
|
+
startStream = sinon_1.default
|
|
132
|
+
.stub(IOSStreamService_1.default.prototype, 'startStream')
|
|
133
|
+
.resolves({ mjpegPort: 9150 });
|
|
134
|
+
});
|
|
135
|
+
const local = (phone) => new LocalSession_1.LocalSession({
|
|
136
|
+
sessionId: 's-video',
|
|
137
|
+
device: phone,
|
|
138
|
+
// The capabilities the driver answered the create with.
|
|
139
|
+
sessionResponse: { udid: phone.udid, wdaLocalPort: 8101, mjpegServerPort: 9101 },
|
|
140
|
+
xenonOption: {},
|
|
141
|
+
driver: { opts: { address: '127.0.0.1', port: 4723, basePath: '/wd/hub' }, sessions: {} },
|
|
142
|
+
});
|
|
143
|
+
it("is read from the simulator's own WebDriverAgent, at the session's MJPEG port", () => __awaiter(this, void 0, void 0, function* () {
|
|
144
|
+
yield local(device(SIMULATOR, false)).startVideoRecording();
|
|
145
|
+
(0, chai_1.expect)(startStream.called, 'started the iPhone stream').to.equal(false);
|
|
146
|
+
(0, chai_1.expect)(startRecording.calledOnce).to.equal(true);
|
|
147
|
+
(0, chai_1.expect)(startRecording.firstCall.args[0]).to.deep.include({
|
|
148
|
+
udid: SIMULATOR,
|
|
149
|
+
mjpegPort: 9101,
|
|
150
|
+
});
|
|
151
|
+
}));
|
|
152
|
+
it("is still read from an iPhone's stream", () => __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
yield local(device(IPHONE, true)).startVideoRecording();
|
|
154
|
+
(0, chai_1.expect)(startStream.calledOnceWith(IPHONE)).to.equal(true);
|
|
155
|
+
(0, chai_1.expect)(startRecording.firstCall.args[0]).to.deep.include({ udid: IPHONE, mjpegPort: 9150 });
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -26,14 +26,31 @@ describe('canOverrideLease — who may use a lease they did not create', () => {
|
|
|
26
26
|
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'api-key', scopes: 'admin', user: null })).to.equal(false);
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
// A session token carries the scopes of the credential that minted it, and
|
|
30
|
+
// is judged by them like a key. By role alone, an ADMIN's key without the
|
|
31
|
+
// admin scope minted itself a token that could override.
|
|
32
|
+
describe("a session token (judged by its scopes and its user's role now)", () => {
|
|
33
|
+
// resolveActor's rule (a SUPER_ADMIN, or a credential with `admin`), on
|
|
34
|
+
// the user's role now: the scope is fixed at mint, so an ADMIN's token
|
|
35
|
+
// also needs the role to still be ADMIN, which notices a demotion.
|
|
36
|
+
it("is allowed for an ADMIN's or SUPER_ADMIN's token carrying the admin scope", () => {
|
|
37
|
+
for (const role of ['ADMIN', 'SUPER_ADMIN']) {
|
|
38
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin,sessions', user: user(role) })).to.equal(true);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// As their key pair overrides, and their Bearer token in per-command auth.
|
|
42
|
+
it("is allowed for a SUPER_ADMIN's token without the admin scope", () => {
|
|
43
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('SUPER_ADMIN') })).to.equal(true);
|
|
44
|
+
});
|
|
45
|
+
it('is refused for a token with the admin scope whose user is now a member', () => {
|
|
46
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin,sessions', user: user('MEMBER') })).to.equal(false);
|
|
47
|
+
});
|
|
48
|
+
it("is refused for an ADMIN's token without the admin scope", () => {
|
|
49
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('ADMIN') })).to.equal(false);
|
|
33
50
|
});
|
|
34
51
|
it('is refused for a member, or a user who is not active', () => {
|
|
35
|
-
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: user('MEMBER') })).to.equal(false);
|
|
36
|
-
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: null })).to.equal(false);
|
|
52
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('MEMBER') })).to.equal(false);
|
|
53
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin', user: null })).to.equal(false);
|
|
37
54
|
});
|
|
38
55
|
});
|
|
39
56
|
it('is allowed with auth disabled, where every caller is a SUPER_ADMIN', () => {
|
|
@@ -83,8 +83,11 @@ const sessionCaps = (xenonOptions, extra = {}) => ({
|
|
|
83
83
|
const key = (accessKey) => ({ accessKey, token: 'tk' });
|
|
84
84
|
const ownerKey = key('ak_owner');
|
|
85
85
|
const otherKey = key('ak_other');
|
|
86
|
-
// A session token names its user in `sub
|
|
87
|
-
|
|
86
|
+
// A session token names its user in `sub` and carries the scopes of the
|
|
87
|
+
// credential that minted it; the fake verifier echoes both back.
|
|
88
|
+
const sessionToken = (sub, scopes = 'sessions') => ({
|
|
89
|
+
sessionToken: `jwt:${sub}|${scopes}`,
|
|
90
|
+
});
|
|
88
91
|
const keys = {
|
|
89
92
|
ak_owner: { id: 'key_owner', userId: 'usr_owner', scopes: 'sessions', teamId: null },
|
|
90
93
|
ak_other: { id: 'key_other', userId: 'usr_other', scopes: 'sessions', teamId: null },
|
|
@@ -174,7 +177,10 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
174
177
|
findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return roles[id] ? { id, role: roles[id], status: 'ACTIVE' } : null; }),
|
|
175
178
|
});
|
|
176
179
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
177
|
-
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
181
|
+
const [sub, scopes] = t.replace(/^jwt:/, '').split('|');
|
|
182
|
+
return { sub, teamId: null, scopes };
|
|
183
|
+
}),
|
|
178
184
|
});
|
|
179
185
|
teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
180
186
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
@@ -331,8 +337,14 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
331
337
|
it("is allowed to a SUPER_ADMIN's key", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
332
338
|
(0, chai_1.expect)(yield allowed(sessionCaps(key('ak_super')))).to.equal(true);
|
|
333
339
|
}));
|
|
340
|
+
// A session token minted from the dashboard carries the admin's admin
|
|
341
|
+
// scope; one minted with an ADMIN's narrower key doesn't, and overrides
|
|
342
|
+
// no more than that key does.
|
|
334
343
|
it('is allowed to an ADMIN presenting a session token, as on the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
335
|
-
(0, chai_1.expect)(yield allowed(sessionCaps(sessionToken('usr_admin')))).to.equal(true);
|
|
344
|
+
(0, chai_1.expect)(yield allowed(sessionCaps(sessionToken('usr_admin', 'admin,sessions')))).to.equal(true);
|
|
345
|
+
}));
|
|
346
|
+
it("is refused to an ADMIN's session token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
347
|
+
(0, chai_1.expect)(yield refusal(sessionCaps(sessionToken('usr_admin', 'sessions')))).to.equal(REFUSED);
|
|
336
348
|
}));
|
|
337
349
|
it('is refused to a member presenting a session token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
338
350
|
(0, chai_1.expect)(yield refusal(sessionCaps(sessionToken('usr_other')))).to.equal(REFUSED);
|
|
@@ -73,6 +73,10 @@ function harness(over) {
|
|
|
73
73
|
const mux = new LogcatMultiplexer_1.LogcatMultiplexer();
|
|
74
74
|
const state = { startCalls: 0, authorizeCalls: 0, authorizeActors: [] };
|
|
75
75
|
const server = http_1.default.createServer();
|
|
76
|
+
const connectionsClosed = [];
|
|
77
|
+
server.on('connection', (socket) => {
|
|
78
|
+
connectionsClosed.push(new Promise((r) => socket.once('close', () => r())));
|
|
79
|
+
});
|
|
76
80
|
const userAuthorize = (_a = over.authorize) !== null && _a !== void 0 ? _a : (() => __awaiter(this, void 0, void 0, function* () { return true; }));
|
|
77
81
|
const deps = {
|
|
78
82
|
redeem: (_b = over.redeem) !== null && _b !== void 0 ? _b : (() => __awaiter(this, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); })),
|
|
@@ -109,6 +113,13 @@ function harness(over) {
|
|
|
109
113
|
get authorizeActors() {
|
|
110
114
|
return state.authorizeActors;
|
|
111
115
|
},
|
|
116
|
+
serverSawClose: () => __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
yield Promise.all(connectionsClosed);
|
|
118
|
+
// ws emits the server WebSocket's 'close' from the TCP socket's 'close'
|
|
119
|
+
// listener, or from a process.nextTick after it (its receiver's
|
|
120
|
+
// 'finish'). Either has run by the event loop's next check phase.
|
|
121
|
+
yield new Promise((r) => setImmediate(r));
|
|
122
|
+
}),
|
|
112
123
|
close: () => new Promise((r) => {
|
|
113
124
|
var _a;
|
|
114
125
|
// http.Server#close only fires its callback once every connection has
|
|
@@ -159,6 +170,50 @@ function connectCollecting(h_1) {
|
|
|
159
170
|
return { code, got };
|
|
160
171
|
});
|
|
161
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Holds one handshake step until the test releases it, and says when the
|
|
175
|
+
* server has reached it, so a test acts while that step is in flight instead
|
|
176
|
+
* of guessing how long the steps before it take. Under load a fixed 50 ms
|
|
177
|
+
* wasn't enough: the client was still CONNECTING, and terminate() there
|
|
178
|
+
* raised an uncaught error.
|
|
179
|
+
*/
|
|
180
|
+
function heldStep() {
|
|
181
|
+
let enter = () => undefined;
|
|
182
|
+
let release = () => undefined;
|
|
183
|
+
const entered = new Promise((r) => (enter = r));
|
|
184
|
+
const released = new Promise((r) => (release = r));
|
|
185
|
+
return {
|
|
186
|
+
entered,
|
|
187
|
+
release,
|
|
188
|
+
/** For the stubbed step: marks it entered, then waits for the release. */
|
|
189
|
+
hold: () => __awaiter(this, void 0, void 0, function* () {
|
|
190
|
+
enter();
|
|
191
|
+
yield released;
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Connects, drops the connection once it is open and `step` is in flight,
|
|
197
|
+
* and resolves when the server has seen it go.
|
|
198
|
+
*/
|
|
199
|
+
function dropDuring(h, step) {
|
|
200
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
const ws = new ws_1.WebSocket(url(h.port));
|
|
202
|
+
// terminate() waits for 'open', so it never raises the 'error' it raises on
|
|
203
|
+
// a CONNECTING socket. Any other unhandled 'error' would be an uncaught
|
|
204
|
+
// exception, failing whichever test is running when it lands.
|
|
205
|
+
ws.on('error', () => undefined);
|
|
206
|
+
yield Promise.all([new Promise((r) => ws.once('open', r)), step.entered]);
|
|
207
|
+
ws.terminate();
|
|
208
|
+
yield h.serverSawClose();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Lets the handshake run on after a held step is released. Everything after
|
|
213
|
+
* a step is promise continuations (the harness's steps do no I/O), so it has
|
|
214
|
+
* all run by the event loop's next check phase.
|
|
215
|
+
*/
|
|
216
|
+
const handshakeSettled = () => new Promise((r) => setImmediate(r));
|
|
162
217
|
describe('attachLogcatWs handshake', () => {
|
|
163
218
|
it('accepts a valid ticket for a device the caller may use', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
219
|
const h = yield harness({});
|
|
@@ -265,26 +320,21 @@ describe('attachLogcatWs handshake', () => {
|
|
|
265
320
|
// The trap documented in h264StreamWs: without cleanup registered before the
|
|
266
321
|
// awaits, clientCount stays inflated and the idle watchdog never fires.
|
|
267
322
|
it('leaves no client registered when the socket drops mid-handshake during redeem', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
268
|
-
|
|
269
|
-
const gate = new Promise((r) => (release = r));
|
|
323
|
+
const step = heldStep();
|
|
270
324
|
const h = yield harness({
|
|
271
325
|
redeem: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
272
|
-
yield
|
|
326
|
+
yield step.hold();
|
|
273
327
|
return { actorId: 'usr_alice' };
|
|
274
328
|
}),
|
|
275
329
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// listener firing late — same end state, different (unguarded) mechanism.
|
|
285
|
-
yield new Promise((r) => setTimeout(r, 100));
|
|
286
|
-
release();
|
|
287
|
-
yield new Promise((r) => setTimeout(r, 150));
|
|
330
|
+
// Disconnect while redeem is still pending, and let the server see the
|
|
331
|
+
// close BEFORE redeem resolves. Otherwise this proves nothing about the
|
|
332
|
+
// post-redeem guard: the client could get registered first and only be
|
|
333
|
+
// torn down afterward by the 'close' listener firing late — same end
|
|
334
|
+
// state, different (unguarded) mechanism.
|
|
335
|
+
yield dropDuring(h, step);
|
|
336
|
+
step.release();
|
|
337
|
+
yield handshakeSettled();
|
|
288
338
|
(0, chai_1.expect)(h.mux.clientCount).to.equal(0);
|
|
289
339
|
// The final clientCount alone can't distinguish "the post-redeem guard
|
|
290
340
|
// short-circuited" from "a later guard cleaned up after the fact" — both
|
|
@@ -301,20 +351,16 @@ describe('attachLogcatWs handshake', () => {
|
|
|
301
351
|
// session owner) when the caller disconnects. Same reasoning as the two
|
|
302
352
|
// neighbours — the observable is the adb spawn that must not happen.
|
|
303
353
|
it('leaves no client registered when the socket drops mid-handshake during authorize', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
304
|
-
|
|
305
|
-
const gate = new Promise((r) => (release = r));
|
|
354
|
+
const step = heldStep();
|
|
306
355
|
const h = yield harness({
|
|
307
356
|
authorize: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
308
|
-
yield
|
|
357
|
+
yield step.hold();
|
|
309
358
|
return true;
|
|
310
359
|
}),
|
|
311
360
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
yield new Promise((r) => setTimeout(r, 100));
|
|
316
|
-
release();
|
|
317
|
-
yield new Promise((r) => setTimeout(r, 150));
|
|
361
|
+
yield dropDuring(h, step); // disconnect while authorize is still pending
|
|
362
|
+
step.release();
|
|
363
|
+
yield handshakeSettled();
|
|
318
364
|
(0, chai_1.expect)(h.authorizeCalls, 'authorize was already in flight').to.equal(1);
|
|
319
365
|
(0, chai_1.expect)(h.startCalls, 'must not start a logcat process once the caller is gone').to.equal(0);
|
|
320
366
|
(0, chai_1.expect)(h.mux.clientCount).to.equal(0);
|
|
@@ -324,25 +370,20 @@ describe('attachLogcatWs handshake', () => {
|
|
|
324
370
|
// but startStream (which can shell out to adb) is still in flight when the
|
|
325
371
|
// caller disconnects.
|
|
326
372
|
it('leaves no client registered when the socket drops mid-handshake during startStream', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
327
|
-
|
|
328
|
-
const gate = new Promise((r) => (release = r));
|
|
373
|
+
const step = heldStep();
|
|
329
374
|
const gatedMux = new LogcatMultiplexer_1.LogcatMultiplexer();
|
|
330
375
|
const h = yield harness({
|
|
331
376
|
startStream: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
332
|
-
yield
|
|
377
|
+
yield step.hold();
|
|
333
378
|
return { mux: gatedMux };
|
|
334
379
|
}),
|
|
335
380
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// post-startStream guard.
|
|
343
|
-
yield new Promise((r) => setTimeout(r, 100));
|
|
344
|
-
release();
|
|
345
|
-
yield new Promise((r) => setTimeout(r, 150));
|
|
381
|
+
// As in the redeem test above: the server sees the close before
|
|
382
|
+
// startStream resolves, or this proves nothing about the post-startStream
|
|
383
|
+
// guard.
|
|
384
|
+
yield dropDuring(h, step);
|
|
385
|
+
step.release();
|
|
386
|
+
yield handshakeSettled();
|
|
346
387
|
(0, chai_1.expect)(gatedMux.clientCount).to.equal(0);
|
|
347
388
|
yield h.close();
|
|
348
389
|
}));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
8
|
+
const logger_1 = require("../../src/logger");
|
|
9
|
+
/**
|
|
10
|
+
* In JSON log mode every line was written through Appium's `info`, whatever
|
|
11
|
+
* its level: a debug line printed at `--log-level info`, and an error went to
|
|
12
|
+
* stdout, since Appium's console sends only its `error` level to stderr. Each
|
|
13
|
+
* line now goes out at its own level, as in text mode.
|
|
14
|
+
*/
|
|
15
|
+
describe('XenonLogger in JSON mode writes each line at its own level', () => {
|
|
16
|
+
let wasJson;
|
|
17
|
+
let base;
|
|
18
|
+
let logger;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
wasJson = logger_1.XenonLogger.isJsonLogging;
|
|
21
|
+
logger_1.XenonLogger.configure({ enableJsonLogging: true });
|
|
22
|
+
base = {
|
|
23
|
+
debug: sinon_1.default.stub(),
|
|
24
|
+
info: sinon_1.default.stub(),
|
|
25
|
+
warn: sinon_1.default.stub(),
|
|
26
|
+
error: sinon_1.default.stub(),
|
|
27
|
+
};
|
|
28
|
+
logger = new logger_1.XenonLogger('probe');
|
|
29
|
+
logger.baseLogger = base;
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
logger_1.XenonLogger.configure({ enableJsonLogging: wasJson });
|
|
33
|
+
});
|
|
34
|
+
for (const level of ['debug', 'info', 'warn', 'error']) {
|
|
35
|
+
it(level, () => {
|
|
36
|
+
logger[level](`a ${level} line`);
|
|
37
|
+
const written = Object.keys(base).filter((l) => base[l].called);
|
|
38
|
+
(0, chai_1.expect)(written).to.deep.equal([level]);
|
|
39
|
+
const entry = JSON.parse(base[level].firstCall.args[0]);
|
|
40
|
+
(0, chai_1.expect)(entry.level).to.equal(level);
|
|
41
|
+
(0, chai_1.expect)(entry.message).to.equal(`a ${level} line`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
8
|
+
const mcpTokenTtl_1 = require("../../src/services/token/mcpTokenTtl");
|
|
9
|
+
/**
|
|
10
|
+
* XENON_MCP_TOKEN_TTL_SEC=abc made every xenon-mcp token's lifetime NaN.
|
|
11
|
+
* A value that isn't a whole number of seconds above 0 now warns and keeps
|
|
12
|
+
* the default.
|
|
13
|
+
*/
|
|
14
|
+
describe('XENON_MCP_TOKEN_TTL_SEC', () => {
|
|
15
|
+
it('is the default (a day) when unset or empty, without a warning', () => {
|
|
16
|
+
const warn = sinon_1.default.stub();
|
|
17
|
+
(0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(undefined, warn)).to.equal(86400);
|
|
18
|
+
(0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)('', warn)).to.equal(86400);
|
|
19
|
+
(0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(' ', warn)).to.equal(86400);
|
|
20
|
+
(0, chai_1.expect)(mcpTokenTtl_1.DEFAULT_MCP_TOKEN_TTL_SEC).to.equal(86400);
|
|
21
|
+
(0, chai_1.expect)(warn.called).to.equal(false);
|
|
22
|
+
});
|
|
23
|
+
it('is the number of seconds it gives', () => {
|
|
24
|
+
const warn = sinon_1.default.stub();
|
|
25
|
+
(0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)('3600', warn)).to.equal(3600);
|
|
26
|
+
(0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(' 120 ', warn)).to.equal(120);
|
|
27
|
+
(0, chai_1.expect)(warn.called).to.equal(false);
|
|
28
|
+
});
|
|
29
|
+
for (const bad of ['abc', '0', '-60', '1.5', '1e400', 'Infinity', '12s']) {
|
|
30
|
+
it(`warns and keeps the default for "${bad}"`, () => {
|
|
31
|
+
const warn = sinon_1.default.stub();
|
|
32
|
+
(0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(bad, warn)).to.equal(86400);
|
|
33
|
+
(0, chai_1.expect)(warn.calledOnce).to.equal(true);
|
|
34
|
+
(0, chai_1.expect)(warn.firstCall.args[0]).to.include('XENON_MCP_TOKEN_TTL_SEC');
|
|
35
|
+
(0, chai_1.expect)(warn.firstCall.args[0]).to.include(bad);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
/**
|
|
11
|
+
* The example Loki config set `limits_config.retention_period: 24h`, but Loki
|
|
12
|
+
* deletes nothing unless its compactor runs retention, so the logs it kept
|
|
13
|
+
* grew for ever. Loki 3 also refuses to start with retention enabled and no
|
|
14
|
+
* store for delete requests.
|
|
15
|
+
*/
|
|
16
|
+
describe('examples/observability/loki.yml', () => {
|
|
17
|
+
const config = js_yaml_1.default.load(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../examples/observability/loki.yml'), 'utf8'));
|
|
18
|
+
it('keeps logs for the retention period it sets', () => {
|
|
19
|
+
var _a, _b, _c;
|
|
20
|
+
(0, chai_1.expect)(config.limits_config.retention_period).to.equal('24h');
|
|
21
|
+
(0, chai_1.expect)((_a = config.compactor) === null || _a === void 0 ? void 0 : _a.retention_enabled, 'compactor.retention_enabled').to.equal(true);
|
|
22
|
+
(0, chai_1.expect)((_b = config.compactor) === null || _b === void 0 ? void 0 : _b.delete_request_store, 'compactor.delete_request_store').to.equal(config.schema_config.configs[0].object_store);
|
|
23
|
+
(0, chai_1.expect)((_c = config.compactor) === null || _c === void 0 ? void 0 : _c.working_directory, 'compactor.working_directory').to.be.a('string');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
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 sharp_1 = __importDefault(require("sharp"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const OcrHealingProvider_1 = require("../../src/services/healing/OcrHealingProvider");
|
|
20
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
21
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
22
|
+
/**
|
|
23
|
+
* The OCR healing tier, on a real screenshot read by a real Tesseract worker
|
|
24
|
+
* (the language data the plugin ships), with nothing stubbed between them.
|
|
25
|
+
*
|
|
26
|
+
* Through 2.14 the tier read `result.data.words` from tesseract.js, which
|
|
27
|
+
* since tesseract.js 6 returns text only unless asked: `words` was undefined,
|
|
28
|
+
* and the tier healed nothing, ever. Its tests stubbed the OCR with words, so
|
|
29
|
+
* none of them could see it. ocr-healing-tier.spec.ts covers the matching
|
|
30
|
+
* rules on stubbed words; this checks the whole tier reads a screen.
|
|
31
|
+
*/
|
|
32
|
+
describe('The OCR healing tier, reading a real screenshot', () => {
|
|
33
|
+
let restoreContainer;
|
|
34
|
+
let screenshot;
|
|
35
|
+
// A phone-like screen: four lines, 200 px apart, each at x 60. "Log out"
|
|
36
|
+
// and "Check in" come first, so a tier that took a lone "Log" for "Login",
|
|
37
|
+
// or a lone "in" for "Sign in", answers with the wrong line.
|
|
38
|
+
const WIDTH = 720;
|
|
39
|
+
const LINES = { 'Log out': 150, 'Check in': 350, 'Sign in': 550, Login: 750 };
|
|
40
|
+
const FONT_SIZE = 56;
|
|
41
|
+
/** Where a line's text is: from its cap height above the baseline to just below it. */
|
|
42
|
+
const band = (line) => ({
|
|
43
|
+
top: LINES[line] - FONT_SIZE,
|
|
44
|
+
bottom: LINES[line] + FONT_SIZE / 4,
|
|
45
|
+
});
|
|
46
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
const text = Object.entries(LINES)
|
|
48
|
+
.map(([words, y]) => `<text x="60" y="${y}" font-family="Helvetica, Arial, sans-serif" font-size="${FONT_SIZE}" fill="#111">${words}</text>`)
|
|
49
|
+
.join('');
|
|
50
|
+
const png = yield (0, sharp_1.default)({
|
|
51
|
+
create: { width: WIDTH, height: 900, channels: 3, background: '#ffffff' },
|
|
52
|
+
})
|
|
53
|
+
.composite([
|
|
54
|
+
{
|
|
55
|
+
input: Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="900">${text}</svg>`),
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
.png()
|
|
59
|
+
.toBuffer();
|
|
60
|
+
screenshot = png.toString('base64');
|
|
61
|
+
// An Omni-Vision of its own, whose worker this suite starts and ends.
|
|
62
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
|
|
63
|
+
typedi_1.Container.set(OmniVisionService_1.OmniVisionService, new OmniVisionService_1.OmniVisionService());
|
|
64
|
+
}));
|
|
65
|
+
after(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
const worker = typedi_1.Container.get(OmniVisionService_1.OmniVisionService).sharedWorker;
|
|
67
|
+
yield (worker === null || worker === void 0 ? void 0 : worker.terminate());
|
|
68
|
+
restoreContainer();
|
|
69
|
+
}));
|
|
70
|
+
/** An Android driver: no element answers an iOS predicate, so the tier says where the text is. */
|
|
71
|
+
const android = { findElement: () => __awaiter(void 0, void 0, void 0, function* () { return Promise.reject(new Error('unsupported locator')); }) };
|
|
72
|
+
const heal = (selector) => new OcrHealingProvider_1.OcrHealingProvider().heal({
|
|
73
|
+
sessionId: 'real-ocr-session',
|
|
74
|
+
driver: android,
|
|
75
|
+
strategy: 'xpath',
|
|
76
|
+
selector,
|
|
77
|
+
screenshotBase64: screenshot,
|
|
78
|
+
});
|
|
79
|
+
/** The healed element's box lies on `line`, starting where its text starts. */
|
|
80
|
+
const expectOnLine = (rect, line) => {
|
|
81
|
+
const { top, bottom } = band(line);
|
|
82
|
+
(0, chai_1.expect)(rect, 'rect').to.be.an('object');
|
|
83
|
+
(0, chai_1.expect)(rect.y, `top of the box, on "${line}"`).to.be.within(top, bottom);
|
|
84
|
+
(0, chai_1.expect)(rect.y + rect.height, `bottom of the box, on "${line}"`).to.be.within(top, bottom);
|
|
85
|
+
(0, chai_1.expect)(rect.x, 'left of the box').to.be.within(40, 80);
|
|
86
|
+
};
|
|
87
|
+
it('finds a two-word text on its line, not a lone word of it read earlier', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const healed = yield heal("//*[@text='Sign in']");
|
|
89
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
90
|
+
(0, chai_1.expect)(healed.id).to.match(/^healed_ocr_/);
|
|
91
|
+
(0, chai_1.expect)(healed.recommendedSelector).to.equal('ocr:text="Sign in"');
|
|
92
|
+
(0, chai_1.expect)(healed.text).to.equal('Sign in');
|
|
93
|
+
expectOnLine(healed.rect, 'Sign in');
|
|
94
|
+
// Both words: wider than "Sign" alone at this size.
|
|
95
|
+
(0, chai_1.expect)(healed.rect.width).to.be.above(150);
|
|
96
|
+
(0, chai_1.expect)(healed.confidence).to.be.within(0.6, 1);
|
|
97
|
+
}));
|
|
98
|
+
it('does not take a word that is only part of the text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
const healed = yield heal("//*[@text='Login']");
|
|
100
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
101
|
+
(0, chai_1.expect)(healed.text).to.equal('Login');
|
|
102
|
+
expectOnLine(healed.rect, 'Login');
|
|
103
|
+
}));
|
|
104
|
+
it('reads the text from the attribute the selector names, whatever its case', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const healed = yield heal("//android.widget.Button[@content-desc='check IN']");
|
|
106
|
+
(0, chai_1.expect)(healed, 'healed').to.not.equal(null);
|
|
107
|
+
(0, chai_1.expect)(healed.text).to.equal('Check in');
|
|
108
|
+
expectOnLine(healed.rect, 'Check in');
|
|
109
|
+
}));
|
|
110
|
+
it('heals nothing when the text is not on the screen', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
(0, chai_1.expect)(yield heal("//*[@text='Sign up']")).to.equal(null);
|
|
112
|
+
}));
|
|
113
|
+
});
|