@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
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const typedi_1 = require("typedi");
|
|
20
20
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
21
21
|
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
@@ -23,6 +23,7 @@ const bug_report_1 = __importDefault(require("../../src/app/routers/bug-report")
|
|
|
23
23
|
const TeamService_1 = require("../../src/services/TeamService");
|
|
24
24
|
const prisma_1 = require("../../src/prisma");
|
|
25
25
|
const seedUser_1 = require("../helpers/seedUser");
|
|
26
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
26
27
|
/**
|
|
27
28
|
* A session's data follows its phone's team, run as real queries (the session
|
|
28
29
|
* list, builds and healing reads build Prisma filters that a stub would only
|
|
@@ -32,6 +33,7 @@ const seedUser_1 = require("../helpers/seedUser");
|
|
|
32
33
|
*/
|
|
33
34
|
describe('team visibility on session data (integration)', function () {
|
|
34
35
|
this.timeout(60000);
|
|
36
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
35
37
|
let sa;
|
|
36
38
|
let alice;
|
|
37
39
|
let teamA;
|
|
@@ -123,21 +125,6 @@ describe('team visibility on session data (integration)', function () {
|
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
}));
|
|
126
|
-
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
127
|
-
const ids = [...Object.values(S), `${S.b}-2`];
|
|
128
|
-
yield prisma_1.prisma.sessionLog.deleteMany({ where: { session_id: { in: ids } } });
|
|
129
|
-
yield prisma_1.prisma.selectorState.deleteMany({
|
|
130
|
-
where: { original_selector: { in: [MUTED, MUTED_B] } },
|
|
131
|
-
});
|
|
132
|
-
yield prisma_1.prisma.session.deleteMany({ where: { id: { in: ids } } });
|
|
133
|
-
yield prisma_1.prisma.build.deleteMany({ where: { id: { in: [mixedBuild.id, teamBBuild.id] } } });
|
|
134
|
-
yield prisma_1.prisma.device.deleteMany({ where: { udid: { in: Object.values(U) } } });
|
|
135
|
-
yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
|
|
136
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
|
|
137
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
|
|
138
|
-
yield sa.cleanup();
|
|
139
|
-
yield alice.cleanup();
|
|
140
|
-
}));
|
|
141
128
|
function app() {
|
|
142
129
|
const a = (0, express_1.default)();
|
|
143
130
|
a.use(express_1.default.json());
|
|
@@ -146,8 +133,8 @@ describe('team visibility on session data (integration)', function () {
|
|
|
146
133
|
bug_report_1.default.register(a);
|
|
147
134
|
return a;
|
|
148
135
|
}
|
|
149
|
-
const asAlice = (url) => (0,
|
|
150
|
-
const asSa = (url) => (0,
|
|
136
|
+
const asAlice = (url) => (0, loopbackRequest_1.default)(app()).get(url).set('Cookie', alice.cookie);
|
|
137
|
+
const asSa = (url) => (0, loopbackRequest_1.default)(app()).get(url).set('Cookie', sa.cookie);
|
|
151
138
|
const unknownBody = (id) => ({ error: true, message: `Session with id ${id} not found` });
|
|
152
139
|
it("lists a member's sessions: shared, their team's, and their own on a phone that is gone", () => __awaiter(this, void 0, void 0, function* () {
|
|
153
140
|
const ids = (yield asAlice('/session')).body.map((s) => s.id);
|
|
@@ -265,7 +252,7 @@ describe('team visibility on session data (integration)', function () {
|
|
|
265
252
|
}));
|
|
266
253
|
});
|
|
267
254
|
it("refuses a bug report for another team's session as for an unknown one", () => __awaiter(this, void 0, void 0, function* () {
|
|
268
|
-
const r = yield (0,
|
|
255
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
269
256
|
.post(`/sessions/${S.b}/bug-report?mode=full`)
|
|
270
257
|
.set('Cookie', alice.cookie)
|
|
271
258
|
.set('Origin', 'http://127.0.0.1');
|
|
@@ -15,25 +15,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
20
20
|
const users_1 = require("../../src/app/routers/users");
|
|
21
|
-
const
|
|
21
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
22
22
|
const seedUser_1 = require("../helpers/seedUser");
|
|
23
23
|
describe('users CRUD flow (integration)', function () {
|
|
24
24
|
this.timeout(60000);
|
|
25
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
25
26
|
let sa;
|
|
26
27
|
let admin;
|
|
27
28
|
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
28
29
|
sa = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Flow SA' });
|
|
29
30
|
admin = yield (0, seedUser_1.seedUser)('ADMIN', { name: 'Flow Admin' });
|
|
30
31
|
}));
|
|
31
|
-
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
32
|
-
yield sa.cleanup();
|
|
33
|
-
yield admin.cleanup();
|
|
34
|
-
// Anything created during the flow with the flow-bob email tag.
|
|
35
|
-
yield prisma_1.prisma.user.deleteMany({ where: { email: { contains: 'flow-bob' } } });
|
|
36
|
-
}));
|
|
37
32
|
function buildApp() {
|
|
38
33
|
const app = (0, express_1.default)();
|
|
39
34
|
app.use(express_1.default.json());
|
|
@@ -44,27 +39,27 @@ describe('users CRUD flow (integration)', function () {
|
|
|
44
39
|
it('SA creates an admin -> admin creates a member -> admin lists', () => __awaiter(this, void 0, void 0, function* () {
|
|
45
40
|
const app = buildApp();
|
|
46
41
|
// ADMIN role-create — should fail with 403 (admin can't create admins).
|
|
47
|
-
const denied = yield (0,
|
|
42
|
+
const denied = yield (0, loopbackRequest_1.default)(app)
|
|
48
43
|
.post('/users')
|
|
49
44
|
.set('Cookie', admin.cookie)
|
|
50
45
|
.send({ email: 'flow-admin2@xenon.local', name: 'Admin 2', role: 'ADMIN' });
|
|
51
46
|
(0, chai_1.expect)(denied.status).to.equal(403);
|
|
52
47
|
// SA creates a member.
|
|
53
48
|
const memberEmail = `flow-bob-${Date.now()}@xenon.local`;
|
|
54
|
-
const created = yield (0,
|
|
49
|
+
const created = yield (0, loopbackRequest_1.default)(app)
|
|
55
50
|
.post('/users')
|
|
56
51
|
.set('Cookie', sa.cookie)
|
|
57
52
|
.send({ email: memberEmail, name: 'Bob', role: 'MEMBER' });
|
|
58
53
|
(0, chai_1.expect)(created.status).to.equal(201);
|
|
59
54
|
(0, chai_1.expect)(created.body.temporaryPassword).to.match(/^[A-Za-z0-9_-]{12,}$/);
|
|
60
55
|
// Admin lists — sees the new member; does NOT see SA or other admins.
|
|
61
|
-
const listAdmin = yield (0,
|
|
56
|
+
const listAdmin = yield (0, loopbackRequest_1.default)(app).get('/users').set('Cookie', admin.cookie);
|
|
62
57
|
(0, chai_1.expect)(listAdmin.status).to.equal(200);
|
|
63
58
|
const emailsAdmin = listAdmin.body.map((u) => u.email);
|
|
64
59
|
(0, chai_1.expect)(emailsAdmin).to.include(memberEmail);
|
|
65
60
|
(0, chai_1.expect)(emailsAdmin).to.not.include(sa.user.email);
|
|
66
61
|
// SA lists — sees everyone.
|
|
67
|
-
const listSA = yield (0,
|
|
62
|
+
const listSA = yield (0, loopbackRequest_1.default)(app).get('/users').set('Cookie', sa.cookie);
|
|
68
63
|
const emailsSA = listSA.body.map((u) => u.email);
|
|
69
64
|
(0, chai_1.expect)(emailsSA).to.include(sa.user.email);
|
|
70
65
|
(0, chai_1.expect)(emailsSA).to.include(admin.user.email);
|
|
@@ -15,13 +15,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
20
20
|
const users_1 = require("../../src/app/routers/users");
|
|
21
21
|
const prisma_1 = require("../../src/prisma");
|
|
22
22
|
const seedUser_1 = require("../helpers/seedUser");
|
|
23
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
23
24
|
describe('users lockout protections (integration)', function () {
|
|
24
25
|
this.timeout(60000);
|
|
26
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
25
27
|
function buildApp() {
|
|
26
28
|
const app = (0, express_1.default)();
|
|
27
29
|
app.use(express_1.default.json());
|
|
@@ -32,7 +34,7 @@ describe('users lockout protections (integration)', function () {
|
|
|
32
34
|
it('SA cannot delete themselves', () => __awaiter(this, void 0, void 0, function* () {
|
|
33
35
|
const me = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Lockout Self' });
|
|
34
36
|
try {
|
|
35
|
-
const r = yield (0,
|
|
37
|
+
const r = yield (0, loopbackRequest_1.default)(buildApp()).delete(`/users/${me.user.id}`).set('Cookie', me.cookie);
|
|
36
38
|
(0, chai_1.expect)(r.status).to.equal(400);
|
|
37
39
|
(0, chai_1.expect)(r.body.error).to.match(/yourself/);
|
|
38
40
|
}
|
|
@@ -43,7 +45,7 @@ describe('users lockout protections (integration)', function () {
|
|
|
43
45
|
it('SA cannot demote themselves', () => __awaiter(this, void 0, void 0, function* () {
|
|
44
46
|
const me = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Lockout Demote-Self' });
|
|
45
47
|
try {
|
|
46
|
-
const r = yield (0,
|
|
48
|
+
const r = yield (0, loopbackRequest_1.default)(buildApp())
|
|
47
49
|
.patch(`/users/${me.user.id}`)
|
|
48
50
|
.set('Cookie', me.cookie)
|
|
49
51
|
.send({ role: 'MEMBER' });
|
|
@@ -121,4 +121,70 @@ describe('EmailService', () => {
|
|
|
121
121
|
}
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
|
+
// A reset link Xenon sends points at XENON_PUBLIC_URL, never the request's
|
|
125
|
+
// Host, so with SMTP but no address set no link is emailed at all. Said once
|
|
126
|
+
// at startup, so an operator upgrading learns why self-service reset went.
|
|
127
|
+
describe('warnAboutPublicUrl', () => {
|
|
128
|
+
it('warns when Xenon could send links but has no XENON_PUBLIC_URL to point them at', () => {
|
|
129
|
+
const orig = {
|
|
130
|
+
url: config_1.config.smtpUrl,
|
|
131
|
+
fb: config_1.config.passwordResetLogFallback,
|
|
132
|
+
pub: config_1.config.publicUrl,
|
|
133
|
+
};
|
|
134
|
+
const svc = new EmailService_1.EmailService();
|
|
135
|
+
const warn = sinon_1.default.stub(svc, 'warnLog');
|
|
136
|
+
try {
|
|
137
|
+
config_1.config.smtpUrl = undefined;
|
|
138
|
+
config_1.config.passwordResetLogFallback = false;
|
|
139
|
+
config_1.config.publicUrl = undefined;
|
|
140
|
+
svc.warnAboutPublicUrl();
|
|
141
|
+
(0, chai_1.expect)(warn.called, 'nothing could be sent anyway').to.equal(false);
|
|
142
|
+
config_1.config.smtpUrl = 'smtp://mail.example.com:587';
|
|
143
|
+
config_1.config.publicUrl = 'https://xenon.example.com';
|
|
144
|
+
svc.warnAboutPublicUrl();
|
|
145
|
+
(0, chai_1.expect)(warn.called).to.equal(false);
|
|
146
|
+
config_1.config.publicUrl = 'not a url';
|
|
147
|
+
svc.warnAboutPublicUrl();
|
|
148
|
+
(0, chai_1.expect)(warn.calledOnce).to.equal(true);
|
|
149
|
+
(0, chai_1.expect)(warn.firstCall.args[0]).to.match(/XENON_PUBLIC_URL/);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
config_1.config.smtpUrl = orig.url;
|
|
153
|
+
config_1.config.passwordResetLogFallback = orig.fb;
|
|
154
|
+
config_1.config.publicUrl = orig.pub;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// The variable also drives each device's dashboard_link, so a value
|
|
158
|
+
// Xenon refuses is said at startup whether or not mail is set up. It was
|
|
159
|
+
// silent without SMTP. The value itself is not logged: it may carry a
|
|
160
|
+
// user name and password, one of the reasons it is refused.
|
|
161
|
+
it('warns, once, when XENON_PUBLIC_URL is set but refused, mail or not', () => {
|
|
162
|
+
const orig = {
|
|
163
|
+
url: config_1.config.smtpUrl,
|
|
164
|
+
fb: config_1.config.passwordResetLogFallback,
|
|
165
|
+
pub: config_1.config.publicUrl,
|
|
166
|
+
};
|
|
167
|
+
const svc = new EmailService_1.EmailService();
|
|
168
|
+
const warn = sinon_1.default.stub(svc, 'warnLog');
|
|
169
|
+
try {
|
|
170
|
+
config_1.config.smtpUrl = undefined;
|
|
171
|
+
config_1.config.passwordResetLogFallback = false;
|
|
172
|
+
config_1.config.publicUrl = 'https://user:secret@lab.example.com/qa';
|
|
173
|
+
svc.warnAboutPublicUrl();
|
|
174
|
+
(0, chai_1.expect)(warn.calledOnce).to.equal(true);
|
|
175
|
+
(0, chai_1.expect)(warn.firstCall.args[0]).to.match(/XENON_PUBLIC_URL is set/);
|
|
176
|
+
(0, chai_1.expect)(warn.firstCall.args[0]).to.match(/dashboard/);
|
|
177
|
+
(0, chai_1.expect)(warn.firstCall.args[0]).to.not.include('secret');
|
|
178
|
+
warn.resetHistory();
|
|
179
|
+
config_1.config.smtpUrl = 'smtp://mail.example.com:587';
|
|
180
|
+
svc.warnAboutPublicUrl();
|
|
181
|
+
(0, chai_1.expect)(warn.calledOnce, 'one warning with SMTP too').to.equal(true);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
config_1.config.smtpUrl = orig.url;
|
|
185
|
+
config_1.config.passwordResetLogFallback = orig.fb;
|
|
186
|
+
config_1.config.publicUrl = orig.pub;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
124
190
|
});
|
|
@@ -13,7 +13,8 @@ require("reflect-metadata");
|
|
|
13
13
|
const chai_1 = require("chai");
|
|
14
14
|
const types_1 = require("../../src/services/healing/types");
|
|
15
15
|
const ResilioTreeHealingProvider_1 = require("../../src/services/healing/ResilioTreeHealingProvider");
|
|
16
|
-
const
|
|
16
|
+
const xmldom_1 = require("@xmldom/xmldom");
|
|
17
|
+
const resilioPath_1 = require("../../src/services/healing/resilioPath");
|
|
17
18
|
describe('ResilioTreeHealingProvider', () => {
|
|
18
19
|
let provider;
|
|
19
20
|
let mockEtalonService;
|
|
@@ -43,32 +44,26 @@ describe('ResilioTreeHealingProvider', () => {
|
|
|
43
44
|
};
|
|
44
45
|
provider = new ResilioTreeHealingProvider_1.ResilioTreeHealingProvider(mockEtalonService);
|
|
45
46
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const buttonNode = linearLayout.children[0];
|
|
55
|
-
const path = new Path([root, bodyNode, hierarchyNode, frameLayout, linearLayout, buttonNode]);
|
|
47
|
+
// The button's id and text both changed, so it is another element as far as
|
|
48
|
+
// its path can tell: Resilio leaves it to Fuzzy XML rather than guess. (This
|
|
49
|
+
// test used to expect a heal, from a path made by an HTML parse and a driver
|
|
50
|
+
// stub that accepted any XPath, including the lowercase one Resilio wrote,
|
|
51
|
+
// which no real driver matches. See resilio-path-healing.spec.ts.)
|
|
52
|
+
it('leaves an element whose id and text changed to the next tier', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
const doc = new xmldom_1.DOMParser().parseFromString(sourceXml, 'text/xml');
|
|
54
|
+
const button = doc.getElementsByTagName('android.widget.Button')[0];
|
|
56
55
|
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
56
|
return ({
|
|
58
57
|
selector: "//android.widget.Button[@text='Submit']",
|
|
59
58
|
strategy: 'xpath',
|
|
60
59
|
attributes: { text: 'Submit', 'resource-id': 'com.example:id/submit_btn' },
|
|
61
60
|
nodeName: 'android.widget.Button',
|
|
62
|
-
path:
|
|
61
|
+
path: (0, resilioPath_1.resilioPathOf)(button, sourceXml),
|
|
63
62
|
lastSeen: Date.now(),
|
|
64
63
|
});
|
|
65
64
|
});
|
|
66
65
|
const mockDriver = {
|
|
67
|
-
findElement: (
|
|
68
|
-
(0, chai_1.expect)(strategy).to.equal('xpath');
|
|
69
|
-
(0, chai_1.expect)(selector.toLowerCase()).to.contain('android.widget.button');
|
|
70
|
-
return { ELEMENT: 'healed-element-123' };
|
|
71
|
-
}),
|
|
66
|
+
findElement: () => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'any-element' }); }),
|
|
72
67
|
};
|
|
73
68
|
const context = {
|
|
74
69
|
sessionId: 'test-session',
|
|
@@ -77,11 +72,43 @@ describe('ResilioTreeHealingProvider', () => {
|
|
|
77
72
|
selector: "//android.widget.Button[@text='Submit']",
|
|
78
73
|
pageSource: brokenXml,
|
|
79
74
|
};
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
(0, chai_1.expect)(yield provider.heal(context)).to.equal(null);
|
|
76
|
+
}));
|
|
77
|
+
it('heals the same element after the page changed around it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
const doc = new xmldom_1.DOMParser().parseFromString(sourceXml, 'text/xml');
|
|
79
|
+
const button = doc.getElementsByTagName('android.widget.Button')[0];
|
|
80
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
return ({
|
|
82
|
+
selector: "//android.widget.Button[@text='Submit']",
|
|
83
|
+
strategy: 'xpath',
|
|
84
|
+
attributes: {},
|
|
85
|
+
nodeName: 'android.widget.Button',
|
|
86
|
+
path: (0, resilioPath_1.resilioPathOf)(button, sourceXml),
|
|
87
|
+
lastSeen: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
// The button moved into a new row; it is still "Submit".
|
|
91
|
+
const movedXml = sourceXml
|
|
92
|
+
.replace('<android.widget.Button index="0"', '<android.widget.LinearLayout index="0"><android.widget.Button index="0"')
|
|
93
|
+
.replace('resource-id="com.example:id/submit_btn" />', 'resource-id="com.example:id/submit_btn" /></android.widget.LinearLayout>');
|
|
94
|
+
const asked = [];
|
|
95
|
+
const result = yield provider.heal({
|
|
96
|
+
sessionId: 'test-session',
|
|
97
|
+
driver: {
|
|
98
|
+
findElement: (_using, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
asked.push(selector);
|
|
100
|
+
return { ELEMENT: 'healed-element-123' };
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
strategy: 'xpath',
|
|
104
|
+
selector: "//android.widget.Button[@text='Submit']",
|
|
105
|
+
pageSource: movedXml,
|
|
106
|
+
});
|
|
82
107
|
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('healed-element-123');
|
|
83
108
|
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.tier).to.equal(types_1.HealingTier.TIER_1_RECOVERY);
|
|
84
109
|
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.message).to.contain('ResilioTree');
|
|
110
|
+
// The element's own id, which it kept.
|
|
111
|
+
(0, chai_1.expect)(asked[0]).to.equal("//*[@resource-id='com.example:id/submit_btn']");
|
|
85
112
|
}));
|
|
86
113
|
it('should return null if no signature/path is found', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
87
114
|
const context = {
|
|
@@ -60,3 +60,26 @@ describe('XenonCapabilityManager.getXenonCapabilities — interceptor activation
|
|
|
60
60
|
expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(true);
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
/**
|
|
64
|
+
* `xe:save_device_logs` is a switch like `xe:record_video`: on by default,
|
|
65
|
+
* on only for `true` or "true" once set. Through 2.15 it was read nowhere.
|
|
66
|
+
*/
|
|
67
|
+
describe('XenonCapabilityManager.getXenonCapabilities — xe:save_device_logs', () => {
|
|
68
|
+
const saved = (always) => (0, XenonCapabilityManager_1.getXenonCapabilities)(caps(always))[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS];
|
|
69
|
+
it('is on when the session says nothing', () => {
|
|
70
|
+
expect(saved({})).to.equal(true);
|
|
71
|
+
});
|
|
72
|
+
it('is on for true or "true"', () => {
|
|
73
|
+
expect(saved({ 'xe:save_device_logs': true })).to.equal(true);
|
|
74
|
+
expect(saved({ 'xe:saveDeviceLogs': 'true' })).to.equal(true);
|
|
75
|
+
});
|
|
76
|
+
it('is off for false, and for any other value, as every switch is', () => {
|
|
77
|
+
for (const value of [false, 'false', 'yes', 1]) {
|
|
78
|
+
expect(saved({ 'xe:save_device_logs': value }), String(value)).to.equal(false);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
it('is read from xe:options and its xenon:options alias too', () => {
|
|
82
|
+
expect(saved({ 'xe:options': { save_device_logs: false } })).to.equal(false);
|
|
83
|
+
expect(saved({ 'xenon:options': { saveDeviceLogs: false } })).to.equal(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
19
|
+
const CircuitBreaker_1 = require("../../src/services/CircuitBreaker");
|
|
20
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
21
|
+
const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
|
|
22
|
+
/**
|
|
23
|
+
* An AI call made while a test command runs has a time limit.
|
|
24
|
+
*
|
|
25
|
+
* The LLM and visual healing tiers run inside a failing findElement, the
|
|
26
|
+
* visual assertion and the screen description inside an execute script, and
|
|
27
|
+
* an ai-icon find inside findElement or device control's Test locator. Only
|
|
28
|
+
* Ollama had a time limit (30 s). The OpenAI and Anthropic SDKs wait up to 10
|
|
29
|
+
* minutes a try, with two retries, and Gemini has none, so one provider that
|
|
30
|
+
* didn't answer held the command, and the test, for as long, past the
|
|
31
|
+
* client's own timeout.
|
|
32
|
+
*/
|
|
33
|
+
describe('AI calls made during a test command', () => {
|
|
34
|
+
const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
|
|
35
|
+
const SCREEN = 'c2NyZWVuc2hvdA==';
|
|
36
|
+
afterEach(() => sinon_1.default.restore());
|
|
37
|
+
/** What a call came to: its answer, or its error. */
|
|
38
|
+
const outcomeOf = (p) => p.then((value) => ({ value, error: undefined }), (error) => ({ value: undefined, error }));
|
|
39
|
+
const calls = [
|
|
40
|
+
{
|
|
41
|
+
name: 'an LLM heal finds nothing',
|
|
42
|
+
call: (s) => s.healLocator({ selector: '//x', strategy: 'xpath', xml: '<hierarchy/>' }),
|
|
43
|
+
expectOutcome: ({ value, error }) => {
|
|
44
|
+
(0, chai_1.expect)(error).to.equal(undefined);
|
|
45
|
+
(0, chai_1.expect)(value).to.equal(null);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'a visual heal or ai-icon find finds nothing',
|
|
50
|
+
call: (s) => s.visualFind(SCREEN, 'the cart icon'),
|
|
51
|
+
expectOutcome: ({ value, error }) => {
|
|
52
|
+
(0, chai_1.expect)(error).to.equal(undefined);
|
|
53
|
+
(0, chai_1.expect)(value).to.equal(null);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "Test locator fails, saying the provider didn't answer",
|
|
58
|
+
call: (s) => s.visualFind(SCREEN, 'the cart icon', { throwOnError: true }),
|
|
59
|
+
expectOutcome: ({ error }) => {
|
|
60
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s");
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "a visual assertion fails, saying the provider didn't answer",
|
|
65
|
+
call: (s) => s.assertVisual(SCREEN, 'The cart is empty'),
|
|
66
|
+
expectOutcome: ({ error }) => {
|
|
67
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s, so the condition was not checked. Try again later.");
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "a screen description fails, saying the provider didn't answer",
|
|
72
|
+
call: (s) => s.describeScreen(SCREEN),
|
|
73
|
+
expectOutcome: ({ error }) => {
|
|
74
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("The AI provider didn't answer within 30 s");
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
for (const { name, call, expectOutcome } of calls) {
|
|
79
|
+
it(`gives up at the limit, cancelling the request: ${name}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
const { model } = ai.use('anthropic');
|
|
82
|
+
ai.answer('hang');
|
|
83
|
+
const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
|
84
|
+
const service = new AIService_1.AIService();
|
|
85
|
+
const outcome = outcomeOf(call(service));
|
|
86
|
+
yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS - 1);
|
|
87
|
+
(0, chai_1.expect)(ai.calls).to.have.length(1);
|
|
88
|
+
(0, chai_1.expect)((_a = ai.calls[0].signal) === null || _a === void 0 ? void 0 : _a.aborted).to.equal(false);
|
|
89
|
+
yield clock.tickAsync(1);
|
|
90
|
+
(0, chai_1.expect)((_b = ai.calls[0].signal) === null || _b === void 0 ? void 0 : _b.aborted).to.equal(true);
|
|
91
|
+
expectOutcome(yield outcome);
|
|
92
|
+
const breaker = CircuitBreaker_1.CIRCUIT_BREAKERS.snapshot().find((b) => b.key === `ai:anthropic:${model}`);
|
|
93
|
+
(0, chai_1.expect)(breaker === null || breaker === void 0 ? void 0 : breaker.consecutiveFailures).to.equal(1);
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
it("Test connection fails at the limit, saying the provider didn't answer", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
+
var _a;
|
|
98
|
+
ai.use('openai');
|
|
99
|
+
ai.answer('hang');
|
|
100
|
+
const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
|
101
|
+
const result = new AIService_1.AIService().testConnection({});
|
|
102
|
+
yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
|
|
103
|
+
(0, chai_1.expect)(yield result).to.deep.equal({
|
|
104
|
+
success: false,
|
|
105
|
+
message: "Connection failed: The AI provider didn't answer within 30 s",
|
|
106
|
+
});
|
|
107
|
+
(0, chai_1.expect)((_a = ai.calls[0].signal) === null || _a === void 0 ? void 0 : _a.aborted).to.equal(true);
|
|
108
|
+
}));
|
|
109
|
+
it('a failing findElement is answered within two time limits, after both AI tiers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
ai.use('gemini');
|
|
111
|
+
ai.answer('hang');
|
|
112
|
+
const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
|
113
|
+
const driver = {
|
|
114
|
+
getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<hierarchy/>'; }),
|
|
115
|
+
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return SCREEN; }),
|
|
116
|
+
findElement: sinon_1.default.stub().rejects(new Error('no such element')),
|
|
117
|
+
};
|
|
118
|
+
let healed = 'not answered';
|
|
119
|
+
const healing = new HealingOrchestrator_1.HealingOrchestrator({})
|
|
120
|
+
// The visual tier (4), then the LLM tier (5).
|
|
121
|
+
.attemptHealing('sess-time-limit', driver, 'id', 'login', [4, 5])
|
|
122
|
+
.then((result) => (healed = result));
|
|
123
|
+
yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
|
|
124
|
+
(0, chai_1.expect)(ai.calls).to.have.length(2);
|
|
125
|
+
yield clock.tickAsync(AIService_1.AI_CALL_TIMEOUT_MS);
|
|
126
|
+
yield healing;
|
|
127
|
+
(0, chai_1.expect)(healed).to.equal(null);
|
|
128
|
+
(0, chai_1.expect)(ai.calls.map((c) => { var _a; return (_a = c.signal) === null || _a === void 0 ? void 0 : _a.aborted; })).to.deep.equal([true, true]);
|
|
129
|
+
}));
|
|
130
|
+
});
|