@xenon-device-management/xenon 2.13.2 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- 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 +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- 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 +85 -71
- 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/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -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 +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- 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/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- 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/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/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/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- 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-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/apple-family.spec.js +36 -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/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -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 +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -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/max-sessions.spec.js +240 -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/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- 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/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- 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.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -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 +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -45,7 +45,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.AI_SERVICE = exports.AIService = void 0;
|
|
48
|
+
exports.AI_SERVICE = exports.AIService = exports.FAILURE_ANALYSIS_TIMEOUT_MS = exports.AI_CALL_TIMEOUT_MS = exports.AITimeoutError = exports.AIProviderPausedError = exports.AIRateLimitedError = void 0;
|
|
49
|
+
exports.parseVisualVerdict = parseVisualVerdict;
|
|
49
50
|
const generative_ai_1 = require("@google/generative-ai");
|
|
50
51
|
const openai_1 = __importDefault(require("openai"));
|
|
51
52
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
@@ -55,13 +56,128 @@ const fs = __importStar(require("fs"));
|
|
|
55
56
|
const path = __importStar(require("path"));
|
|
56
57
|
const config_1 = require("../config");
|
|
57
58
|
const CircuitBreaker_1 = require("./CircuitBreaker");
|
|
59
|
+
/**
|
|
60
|
+
* A provider refused a call for its rate limit or quota (HTTP 429), whichever
|
|
61
|
+
* provider it is. A failed call like any other: its caller gets no answer and
|
|
62
|
+
* the circuit breaker counts it (`status`). Through 2.14 Gemini's provider
|
|
63
|
+
* answered a 429 with the text 'CONNECTION_OK_RATE_LIMITED', which was saved
|
|
64
|
+
* as a failed session's AI analysis and counted by the breaker as a success.
|
|
65
|
+
*/
|
|
66
|
+
class AIRateLimitedError extends Error {
|
|
67
|
+
constructor(detail) {
|
|
68
|
+
super('The AI provider is rate-limited or out of quota');
|
|
69
|
+
this.name = 'AIRateLimitedError';
|
|
70
|
+
this.status = 429;
|
|
71
|
+
this.detail = detail;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.AIRateLimitedError = AIRateLimitedError;
|
|
75
|
+
/** A provider's 429, as its SDK (`status`) or axios (`response.status`) reports it. */
|
|
76
|
+
function isRateLimit(err) {
|
|
77
|
+
var _a;
|
|
78
|
+
return err instanceof AIRateLimitedError || (err === null || err === void 0 ? void 0 : err.status) === 429 || ((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) === 429;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The circuit breaker didn't send the call: the provider's recent calls
|
|
82
|
+
* failed. Still a CircuitOpenError, in words for testers: the Omni-Scan, Test
|
|
83
|
+
* locator and visual assertion answers show it, and "Circuit open for
|
|
84
|
+
* 'ai:gemini:<model>'; retry in 43120ms" isn't.
|
|
85
|
+
*/
|
|
86
|
+
class AIProviderPausedError extends CircuitBreaker_1.CircuitOpenError {
|
|
87
|
+
constructor(from) {
|
|
88
|
+
super(from.key, from.retryAfterMs);
|
|
89
|
+
this.seconds = Math.max(1, Math.ceil(from.retryAfterMs / 1000));
|
|
90
|
+
this.message = `The AI provider has been failing, so Xenon will call it again in ${this.seconds} s`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.AIProviderPausedError = AIProviderPausedError;
|
|
94
|
+
/** A call that outlasted its time limit; its request was cancelled. The circuit breaker counts it (`code`). */
|
|
95
|
+
class AITimeoutError extends Error {
|
|
96
|
+
constructor(ms) {
|
|
97
|
+
super(`The AI provider didn't answer within ${Math.round(ms / 1000)} s`);
|
|
98
|
+
this.name = 'AITimeoutError';
|
|
99
|
+
this.code = 'ETIMEDOUT';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.AITimeoutError = AITimeoutError;
|
|
103
|
+
/**
|
|
104
|
+
* How long an AI call may take, retries included, unless it says otherwise
|
|
105
|
+
* (failure analysis). Most are made while a test command runs: the LLM and
|
|
106
|
+
* visual healing tiers inside a failing findElement (both: 2 of these), the
|
|
107
|
+
* visual assertion and the screen description inside an execute script, an
|
|
108
|
+
* ai-icon find. The OpenAI and Anthropic SDKs otherwise wait up to 10 minutes
|
|
109
|
+
* a try, three tries, and Gemini has no limit, which held the command past
|
|
110
|
+
* the client's own timeout. Ollama's generate call already had 30 s.
|
|
111
|
+
*/
|
|
112
|
+
exports.AI_CALL_TIMEOUT_MS = 30000;
|
|
113
|
+
/**
|
|
114
|
+
* How long a failed session's AI analysis may take, retries included. The
|
|
115
|
+
* OpenAI and Anthropic SDKs otherwise wait up to 10 minutes a try, three
|
|
116
|
+
* tries. The analysis runs after the session has ended (onSessionStopped
|
|
117
|
+
* doesn't wait for it), so this bounds the work left running, not a client's
|
|
118
|
+
* wait: an answer with a screenshot usually takes seconds.
|
|
119
|
+
*/
|
|
120
|
+
exports.FAILURE_ANALYSIS_TIMEOUT_MS = 120000;
|
|
121
|
+
/** `call`, given a signal that cancels it after `ms`, when it fails with AITimeoutError. */
|
|
122
|
+
function withTimeLimit(ms, call) {
|
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
let timer;
|
|
126
|
+
const expired = new Promise((_, reject) => {
|
|
127
|
+
timer = setTimeout(() => {
|
|
128
|
+
// The time-out first, so it, not the cancelled request's error, is the answer.
|
|
129
|
+
reject(new AITimeoutError(ms));
|
|
130
|
+
controller.abort();
|
|
131
|
+
}, ms);
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
return yield Promise.race([call(controller.signal), expired]);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A provider's answer to the assertion prompt, `{ "result": true|false,
|
|
143
|
+
* "reason": "..." }`, read leniently (code fences, text around the JSON,
|
|
144
|
+
* `message` for `reason`, "true"/"false" as strings) but never guessed: null
|
|
145
|
+
* when it holds no true/false `result`.
|
|
146
|
+
*/
|
|
147
|
+
function parseVisualVerdict(answer) {
|
|
148
|
+
const text = answer.replace(/```(?:json)?/gi, '').trim();
|
|
149
|
+
const start = text.indexOf('{');
|
|
150
|
+
const end = text.lastIndexOf('}');
|
|
151
|
+
if (start === -1 || end <= start)
|
|
152
|
+
return null;
|
|
153
|
+
let data;
|
|
154
|
+
try {
|
|
155
|
+
data = JSON.parse(text.slice(start, end + 1));
|
|
156
|
+
}
|
|
157
|
+
catch (_a) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const raw = data === null || data === void 0 ? void 0 : data.result;
|
|
161
|
+
const result = raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
|
|
162
|
+
if (result === null)
|
|
163
|
+
return null;
|
|
164
|
+
const reason = typeof data.reason === 'string'
|
|
165
|
+
? data.reason
|
|
166
|
+
: typeof data.message === 'string'
|
|
167
|
+
? data.message
|
|
168
|
+
: '';
|
|
169
|
+
return {
|
|
170
|
+
result,
|
|
171
|
+
message: reason.trim() || (result ? 'The condition holds.' : 'The condition does not hold.'),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
58
174
|
class GeminiProvider {
|
|
59
175
|
constructor(apiKey, modelName) {
|
|
60
176
|
this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey.trim());
|
|
61
177
|
this.modelName =
|
|
62
178
|
modelName && modelName.trim() !== '' ? modelName.trim() : 'gemini-3-flash-preview';
|
|
63
179
|
}
|
|
64
|
-
analyze(prompt, screenshotBase64) {
|
|
180
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
65
181
|
return __awaiter(this, void 0, void 0, function* () {
|
|
66
182
|
const parts = [prompt];
|
|
67
183
|
if (screenshotBase64) {
|
|
@@ -80,17 +196,17 @@ class GeminiProvider {
|
|
|
80
196
|
for (const version of versions) {
|
|
81
197
|
try {
|
|
82
198
|
const model = this.genAI.getGenerativeModel({ model: this.modelName }, { apiVersion: version });
|
|
83
|
-
const result = yield model.generateContent(parts);
|
|
199
|
+
const result = yield model.generateContent(parts, signal ? { signal } : undefined);
|
|
84
200
|
const response = yield result.response;
|
|
85
201
|
return response.text();
|
|
86
202
|
}
|
|
87
203
|
catch (err) {
|
|
88
204
|
lastError = err;
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
205
|
+
// The other endpoint has the same quota. By status only: the message
|
|
206
|
+
// holds the URL and the provider's text, where "429" can be a token
|
|
207
|
+
// count or part of the model's name.
|
|
208
|
+
if (err.status === 429)
|
|
209
|
+
throw new AIRateLimitedError(err.message);
|
|
94
210
|
if (err.message.includes('404') ||
|
|
95
211
|
err.message.includes('not found') ||
|
|
96
212
|
err.message.includes('unsupported')) {
|
|
@@ -109,7 +225,7 @@ class OpenAIProvider {
|
|
|
109
225
|
this.client = new openai_1.default({ apiKey, baseURL });
|
|
110
226
|
this.model = model;
|
|
111
227
|
}
|
|
112
|
-
analyze(prompt, screenshotBase64) {
|
|
228
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
113
229
|
return __awaiter(this, void 0, void 0, function* () {
|
|
114
230
|
const messages = [
|
|
115
231
|
{
|
|
@@ -127,7 +243,7 @@ class OpenAIProvider {
|
|
|
127
243
|
model: this.model,
|
|
128
244
|
messages,
|
|
129
245
|
max_tokens: 500,
|
|
130
|
-
});
|
|
246
|
+
}, { signal });
|
|
131
247
|
return response.choices[0].message.content || '';
|
|
132
248
|
});
|
|
133
249
|
}
|
|
@@ -137,7 +253,7 @@ class AnthropicProvider {
|
|
|
137
253
|
this.client = new sdk_1.default({ apiKey });
|
|
138
254
|
this.model = model;
|
|
139
255
|
}
|
|
140
|
-
analyze(prompt, screenshotBase64) {
|
|
256
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
141
257
|
return __awaiter(this, void 0, void 0, function* () {
|
|
142
258
|
const content = [{ type: 'text', text: prompt }];
|
|
143
259
|
if (screenshotBase64) {
|
|
@@ -154,7 +270,7 @@ class AnthropicProvider {
|
|
|
154
270
|
model: this.model,
|
|
155
271
|
max_tokens: 500,
|
|
156
272
|
messages: [{ role: 'user', content }],
|
|
157
|
-
});
|
|
273
|
+
}, { signal });
|
|
158
274
|
return response.content[0].text || '';
|
|
159
275
|
});
|
|
160
276
|
}
|
|
@@ -170,10 +286,11 @@ class OllamaProvider {
|
|
|
170
286
|
checkAvailability() {
|
|
171
287
|
return __awaiter(this, void 0, void 0, function* () {
|
|
172
288
|
const now = Date.now();
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
289
|
+
// An answer is kept for a while; "down" is checked again at the next call
|
|
290
|
+
// (a refused local connection costs nothing), so Ollama is used as soon as
|
|
291
|
+
// it is back.
|
|
292
|
+
if (this.isAvailable && now - this.lastAvailabilityCheck < this.AVAILABILITY_CHECK_INTERVAL) {
|
|
293
|
+
return true;
|
|
177
294
|
}
|
|
178
295
|
try {
|
|
179
296
|
// Quick health check - Ollama has a /api/tags endpoint
|
|
@@ -189,7 +306,7 @@ class OllamaProvider {
|
|
|
189
306
|
}
|
|
190
307
|
});
|
|
191
308
|
}
|
|
192
|
-
analyze(prompt, screenshotBase64) {
|
|
309
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
193
310
|
return __awaiter(this, void 0, void 0, function* () {
|
|
194
311
|
var _a;
|
|
195
312
|
// Check if Ollama is available before attempting
|
|
@@ -203,7 +320,7 @@ class OllamaProvider {
|
|
|
203
320
|
prompt: prompt,
|
|
204
321
|
images: screenshotBase64 ? [screenshotBase64] : [],
|
|
205
322
|
stream: false,
|
|
206
|
-
}, { timeout: 30000 });
|
|
323
|
+
}, { timeout: 30000, signal });
|
|
207
324
|
return response.data.response || '';
|
|
208
325
|
}
|
|
209
326
|
catch (err) {
|
|
@@ -221,6 +338,8 @@ class AIService {
|
|
|
221
338
|
constructor() {
|
|
222
339
|
this.provider = null;
|
|
223
340
|
this.isMock = false;
|
|
341
|
+
/** The settings `provider` was set up from (initializeProvider). */
|
|
342
|
+
this.setUpFrom = null;
|
|
224
343
|
this.initializeProvider();
|
|
225
344
|
}
|
|
226
345
|
// Key the breaker by provider + resolved model name so a misbehaving model
|
|
@@ -240,16 +359,60 @@ class AIService {
|
|
|
240
359
|
return `ai:${provider}:${model || 'default'}`;
|
|
241
360
|
}
|
|
242
361
|
// Single choke point for every LLM call so circuit-breaker state is shared
|
|
243
|
-
// across analyzeFailure / visualFind / healLocator.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
362
|
+
// across analyzeFailure / visualFind / healLocator. A rate limit is
|
|
363
|
+
// AIRateLimitedError whichever provider sent it; a call is cancelled after
|
|
364
|
+
// `timeoutMs` (AI_CALL_TIMEOUT_MS unless given: AITimeoutError). The breaker
|
|
365
|
+
// counts both, and a call it holds back is AIProviderPausedError.
|
|
366
|
+
callProvider(prompt_1, screenshotBase64_1) {
|
|
367
|
+
return __awaiter(this, arguments, void 0, function* (prompt, screenshotBase64, opts = {}) {
|
|
368
|
+
var _a;
|
|
369
|
+
const provider = this.provider;
|
|
370
|
+
try {
|
|
371
|
+
return yield CircuitBreaker_1.CIRCUIT_BREAKERS.execute(this.breakerKey(), () => {
|
|
372
|
+
var _a;
|
|
373
|
+
return withTimeLimit((_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : exports.AI_CALL_TIMEOUT_MS, (signal) => provider.analyze(prompt, screenshotBase64, signal));
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
if (isRateLimit(err) && !(err instanceof AIRateLimitedError)) {
|
|
378
|
+
throw new AIRateLimitedError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
|
|
379
|
+
}
|
|
380
|
+
if (err instanceof CircuitBreaker_1.CircuitOpenError && !(err instanceof AIProviderPausedError)) {
|
|
381
|
+
throw new AIProviderPausedError(err);
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
247
385
|
});
|
|
248
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Sets the provider up from the server's AI settings, which the AI engine
|
|
389
|
+
* page changes while the server runs (`POST /config`). Again only when they
|
|
390
|
+
* changed, so a provider's client, and Ollama's availability check, carry
|
|
391
|
+
* over from one call to the next.
|
|
392
|
+
*/
|
|
249
393
|
initializeProvider() {
|
|
250
394
|
const providerType = config_1.config.aiProvider;
|
|
251
395
|
const genericModel = config_1.config.aiModel;
|
|
252
396
|
const baseUrl = config_1.config.aiBaseUrl;
|
|
397
|
+
const settings = JSON.stringify([
|
|
398
|
+
providerType,
|
|
399
|
+
genericModel,
|
|
400
|
+
baseUrl,
|
|
401
|
+
config_1.config.geminiApiKey,
|
|
402
|
+
config_1.config.openaiApiKey,
|
|
403
|
+
config_1.config.anthropicApiKey,
|
|
404
|
+
config_1.config.geminiModel,
|
|
405
|
+
config_1.config.openaiModel,
|
|
406
|
+
config_1.config.anthropicModel,
|
|
407
|
+
config_1.config.ollamaModel,
|
|
408
|
+
process.env.GEMINI_API_KEY === 'mock',
|
|
409
|
+
]);
|
|
410
|
+
if (settings === this.setUpFrom)
|
|
411
|
+
return;
|
|
412
|
+
this.setUpFrom = settings;
|
|
413
|
+
// A provider chosen without a key is no provider, never the previous one.
|
|
414
|
+
this.provider = null;
|
|
415
|
+
this.isMock = false;
|
|
253
416
|
logger_1.default.info(`[AIService] Initializing with provider: ${providerType}`);
|
|
254
417
|
if (process.env.GEMINI_API_KEY === 'mock') {
|
|
255
418
|
this.isMock = true;
|
|
@@ -285,7 +448,15 @@ class AIService {
|
|
|
285
448
|
logger_1.default.error(`[AIService] Initialization error: ${err.message}`);
|
|
286
449
|
}
|
|
287
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Whether a provider is set up for the settings in force now. Failure
|
|
453
|
+
* analysis and the LLM and visual healing tiers ask this first; it used to
|
|
454
|
+
* answer for the provider set up last, so choosing a configured provider on
|
|
455
|
+
* the AI engine page never turned them on, and choosing one without a key
|
|
456
|
+
* left the previous provider answering.
|
|
457
|
+
*/
|
|
288
458
|
isEnabled() {
|
|
459
|
+
this.initializeProvider();
|
|
289
460
|
return this.provider !== null || this.isMock;
|
|
290
461
|
}
|
|
291
462
|
analyzeFailure(context) {
|
|
@@ -305,7 +476,9 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
305
476
|
try {
|
|
306
477
|
const prompt = this.constructPrompt(context);
|
|
307
478
|
const screenshotBase64 = this.getScreenshotBase64(context.screenshotPath);
|
|
308
|
-
const text = yield this.callProvider(prompt, screenshotBase64 || undefined
|
|
479
|
+
const text = yield this.callProvider(prompt, screenshotBase64 || undefined, {
|
|
480
|
+
timeoutMs: exports.FAILURE_ANALYSIS_TIMEOUT_MS,
|
|
481
|
+
});
|
|
309
482
|
logger_1.default.info(`[AIService] Analysis complete for ${context.sessionId}`);
|
|
310
483
|
return text;
|
|
311
484
|
}
|
|
@@ -313,6 +486,12 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
313
486
|
if (err instanceof CircuitBreaker_1.CircuitOpenError) {
|
|
314
487
|
logger_1.default.debug(`[AIService] Analysis skipped: ${err.message}`);
|
|
315
488
|
}
|
|
489
|
+
else if (err instanceof AIRateLimitedError) {
|
|
490
|
+
logger_1.default.warn(`[AIService] No analysis for ${context.sessionId}: ${err.message}: ${err.detail}`);
|
|
491
|
+
}
|
|
492
|
+
else if (err instanceof AITimeoutError) {
|
|
493
|
+
logger_1.default.warn(`[AIService] No analysis for ${context.sessionId}: ${err.message}`);
|
|
494
|
+
}
|
|
316
495
|
else {
|
|
317
496
|
logger_1.default.error(`[AIService] Analysis failed: ${err.message}`);
|
|
318
497
|
}
|
|
@@ -365,6 +544,71 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
365
544
|
}
|
|
366
545
|
});
|
|
367
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Asks the provider whether `condition` holds on the screenshot
|
|
549
|
+
* (`xenon: assertVisualState`). Answers only with the provider's own
|
|
550
|
+
* verdict: with no provider, a failed call, a rate limit or an answer that
|
|
551
|
+
* isn't true or false it throws, saying the condition was not checked. A
|
|
552
|
+
* `false` there would pass a test asserting that something is absent.
|
|
553
|
+
*/
|
|
554
|
+
assertVisual(screenshotBase64, condition) {
|
|
555
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
556
|
+
var _a;
|
|
557
|
+
this.initializeProvider();
|
|
558
|
+
const notChecked = 'so the condition was not checked';
|
|
559
|
+
if (!this.provider) {
|
|
560
|
+
throw new Error(`No AI provider is configured, ${notChecked}. Set XENON_AI_PROVIDER and the provider's key.`);
|
|
561
|
+
}
|
|
562
|
+
const prompt = `
|
|
563
|
+
You are checking a screenshot of a mobile app for an automated test.
|
|
564
|
+
|
|
565
|
+
Condition: ${JSON.stringify(condition)}
|
|
566
|
+
|
|
567
|
+
Is the condition true of what the screenshot shows? Judge only what is visible on the screen.
|
|
568
|
+
Answer with JSON only, exactly: {"result": true or false, "reason": "one sentence on what you see"}
|
|
569
|
+
`.trim();
|
|
570
|
+
let answer;
|
|
571
|
+
try {
|
|
572
|
+
answer = yield this.callProvider(prompt, screenshotBase64);
|
|
573
|
+
}
|
|
574
|
+
catch (err) {
|
|
575
|
+
if (err instanceof AIRateLimitedError || err instanceof AITimeoutError) {
|
|
576
|
+
throw new Error(`${err.message}, ${notChecked}. Try again later.`);
|
|
577
|
+
}
|
|
578
|
+
if (err instanceof AIProviderPausedError) {
|
|
579
|
+
throw new Error(`The AI provider has been failing, ${notChecked}. Xenon will call it again in ${err.seconds} s.`);
|
|
580
|
+
}
|
|
581
|
+
throw new Error(`The AI provider failed, ${notChecked}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
582
|
+
}
|
|
583
|
+
const verdict = parseVisualVerdict(answer);
|
|
584
|
+
if (!verdict) {
|
|
585
|
+
const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
586
|
+
throw new Error(`Xenon could not read the AI provider's answer as true or false, ${notChecked}. ` +
|
|
587
|
+
`It answered: ${JSON.stringify(said)}`);
|
|
588
|
+
}
|
|
589
|
+
return verdict;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* A short description of the screen in the screenshot, for
|
|
594
|
+
* `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
|
|
595
|
+
* is no provider or the call fails (AIRateLimitedError for a rate limit,
|
|
596
|
+
* AITimeoutError after AI_CALL_TIMEOUT_MS), so the caller can say why there
|
|
597
|
+
* is none.
|
|
598
|
+
*/
|
|
599
|
+
describeScreen(screenshotBase64) {
|
|
600
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
601
|
+
this.initializeProvider();
|
|
602
|
+
if (!this.provider)
|
|
603
|
+
throw new Error('No AI provider is configured');
|
|
604
|
+
const prompt = `
|
|
605
|
+
You are looking at a screenshot of a mobile app, for a software tester.
|
|
606
|
+
Describe the screen in a few short sentences: which screen it is, its main elements and their state (for example filled in, empty, selected or disabled), and anything unexpected, such as an error message, a system dialog, a crash or a loading indicator.
|
|
607
|
+
Describe only what is visible. Answer in plain text.
|
|
608
|
+
`.trim();
|
|
609
|
+
return this.callProvider(prompt, screenshotBase64);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
368
612
|
/**
|
|
369
613
|
* Heals a broken locator using deep LLM reasoning (Tier 5)
|
|
370
614
|
*/
|
|
@@ -413,9 +657,10 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
413
657
|
}
|
|
414
658
|
testConnection(testConfig) {
|
|
415
659
|
return __awaiter(this, void 0, void 0, function* () {
|
|
660
|
+
var _a;
|
|
661
|
+
const providerType = testConfig.aiProvider || config_1.config.aiProvider;
|
|
416
662
|
try {
|
|
417
663
|
let testProvider = null;
|
|
418
|
-
const providerType = testConfig.aiProvider || config_1.config.aiProvider;
|
|
419
664
|
const model = testConfig.aiModel || config_1.config.aiModel;
|
|
420
665
|
const baseUrl = testConfig.aiBaseUrl || config_1.config.aiBaseUrl;
|
|
421
666
|
logger_1.default.info(`[AIService] Testing connection for provider: ${providerType}`);
|
|
@@ -448,17 +693,21 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
448
693
|
}
|
|
449
694
|
if (!testProvider)
|
|
450
695
|
throw new Error('Failed to initialize provider for testing');
|
|
451
|
-
// Send a minimal ping command
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
success: true,
|
|
456
|
-
message: `Successfully connected to ${providerType}! (Note: You are currently out of quota/rate-limited, but the setup is correct)`,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
696
|
+
// Send a minimal ping command. Not through the circuit breaker (a test
|
|
697
|
+
// shouldn't pause the provider for everyone), but with the time limit.
|
|
698
|
+
const pinged = testProvider;
|
|
699
|
+
yield withTimeLimit(exports.AI_CALL_TIMEOUT_MS, (signal) => pinged.analyze('Hello. Response: OK', undefined, signal));
|
|
459
700
|
return { success: true, message: `Successfully connected to ${providerType}!` };
|
|
460
701
|
}
|
|
461
702
|
catch (err) {
|
|
703
|
+
// Not a success: every AI call fails the same way until it has quota again.
|
|
704
|
+
if (isRateLimit(err)) {
|
|
705
|
+
logger_1.default.warn(`[AIService] Connection test: ${providerType} is rate-limited: ${(_a = err.detail) !== null && _a !== void 0 ? _a : err.message}`);
|
|
706
|
+
return {
|
|
707
|
+
success: false,
|
|
708
|
+
message: `${providerType} answered, but it is rate-limited or out of quota. AI features won't work with it until it has quota again.`,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
462
711
|
logger_1.default.error(`[AIService] Connection test failed: ${err.message}`);
|
|
463
712
|
return { success: false, message: `Connection failed: ${err.message}` };
|
|
464
713
|
}
|
|
@@ -25,6 +25,7 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
25
25
|
const fs_1 = __importDefault(require("fs"));
|
|
26
26
|
const path_1 = __importDefault(require("path"));
|
|
27
27
|
const config_1 = require("../config");
|
|
28
|
+
const labSettings_1 = require("./settings/labSettings");
|
|
28
29
|
const recordingCleanup_1 = require("./recording/recordingCleanup");
|
|
29
30
|
/** Subdirectory holding composite (multi-device) outputs, keyed by group id. */
|
|
30
31
|
const GROUPS_DIR = '_groups';
|
|
@@ -34,6 +35,8 @@ let CleanupService = class CleanupService {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Orchestrates build + session + recording cleanup based on retention policy.
|
|
38
|
+
* The build retention window, the build cap and the asset purge are the ones
|
|
39
|
+
* saved on the dashboard's Maintenance page, else `pluginArgs`'.
|
|
37
40
|
*
|
|
38
41
|
* Phases:
|
|
39
42
|
* 1. Purge old builds (by age AND count cap) together with their sessions.
|
|
@@ -44,7 +47,9 @@ let CleanupService = class CleanupService {
|
|
|
44
47
|
*/
|
|
45
48
|
runCleanup(pluginArgs) {
|
|
46
49
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
|
|
50
|
+
// What the Maintenance page saved wins over the startup options, and is
|
|
51
|
+
// read here, at each run, so a change needs no restart. See labSettings.ts.
|
|
52
|
+
const { buildCleanupDays, buildCleanupMaxCount, deleteBuildAssets } = yield (0, labSettings_1.loadEffectiveSettings)(pluginArgs);
|
|
48
53
|
this.log.info(`Starting cleanup: Retention = ${buildCleanupDays} days, Max Builds = ${buildCleanupMaxCount}, Purge Assets = ${deleteBuildAssets}`);
|
|
49
54
|
const expirationDate = new Date();
|
|
50
55
|
expirationDate.setDate(expirationDate.getDate() - buildCleanupDays);
|
|
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
24
24
|
const config_1 = require("../config");
|
|
25
25
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
|
+
const passwordResetLink_1 = require("./passwordResetLink");
|
|
26
27
|
let EmailService = class EmailService {
|
|
27
28
|
constructor() {
|
|
28
29
|
this.log = logger_1.default.scope('Email');
|
|
@@ -55,6 +56,34 @@ let EmailService = class EmailService {
|
|
|
55
56
|
'over the account for the link lifetime. Configure XENON_SMTP_URL, or unset this and ' +
|
|
56
57
|
'issue links from the dashboard Users page.');
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Startup notice about XENON_PUBLIC_URL, the address links Xenon hands out
|
|
61
|
+
* are built from (never the request's Host, which the asker chooses):
|
|
62
|
+
*
|
|
63
|
+
* - set but refused (not an http(s) server address): whatever the mail
|
|
64
|
+
* setup, since it also decides each device's dashboard_link. The value is
|
|
65
|
+
* not logged: a user name and password in it is one reason to refuse it.
|
|
66
|
+
* - unset while Xenon could send reset links: the sign-in page's "forgot
|
|
67
|
+
* password" then sends people to an administrator, and an administrator's
|
|
68
|
+
* reset link is handed back to them rather than emailed.
|
|
69
|
+
*/
|
|
70
|
+
warnAboutPublicUrl() {
|
|
71
|
+
var _a;
|
|
72
|
+
if (((_a = config_1.config.publicUrl) === null || _a === void 0 ? void 0 : _a.trim()) && (0, passwordResetLink_1.resetLinkBase)() === null) {
|
|
73
|
+
this.warnLog('XENON_PUBLIC_URL is set but is not an http(s) server address such as ' +
|
|
74
|
+
'https://xenon.example.com or http://lab-mac:4723 (a trailing /xenon is fine; any ' +
|
|
75
|
+
'other path, a query, a fragment or a user name is not), so Xenon ignores it: it ' +
|
|
76
|
+
'emails no password-reset links, and device dashboard links are paths on this server.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!this.canDeliver() || (0, passwordResetLink_1.resetLinkBase)() !== null)
|
|
80
|
+
return;
|
|
81
|
+
this.warnLog("XENON_PUBLIC_URL is not set to this server's address, so Xenon sends no password-reset " +
|
|
82
|
+
'links: "Forgot password" asks people to contact an administrator, and the Users ' +
|
|
83
|
+
'page hands an administrator the link instead of emailing it. Set XENON_PUBLIC_URL to ' +
|
|
84
|
+
'the http(s) address people reach this server at, such as https://xenon.example.com ' +
|
|
85
|
+
'or http://lab-mac:4723 (a trailing /xenon is fine; any other path is not).');
|
|
86
|
+
}
|
|
58
87
|
send(mail) {
|
|
59
88
|
return __awaiter(this, void 0, void 0, function* () {
|
|
60
89
|
const smtpUrl = config_1.config.smtpUrl;
|
|
@@ -72,6 +72,10 @@ const SessionArchive_1 = require("./interceptor/SessionArchive");
|
|
|
72
72
|
const PortAllocator_1 = require("./PortAllocator");
|
|
73
73
|
const SocketServer_1 = require("./SocketServer");
|
|
74
74
|
const SocketEvents_1 = require("../enums/SocketEvents");
|
|
75
|
+
const adbForPhone_1 = require("./network/adbForPhone");
|
|
76
|
+
const PhoneNetworkLedger_1 = require("./network/PhoneNetworkLedger");
|
|
77
|
+
const phoneNetworkLock_1 = require("./network/phoneNetworkLock");
|
|
78
|
+
const xenonProxy_1 = require("./network/xenonProxy");
|
|
75
79
|
const DEFAULT_BUFFER_CAP = 1000;
|
|
76
80
|
const BODY_SPILL_THRESHOLD = 1024 * 1024;
|
|
77
81
|
let InterceptorService = class InterceptorService {
|
|
@@ -88,6 +92,10 @@ let InterceptorService = class InterceptorService {
|
|
|
88
92
|
isActive(sessionId) {
|
|
89
93
|
return this.states.has(sessionId);
|
|
90
94
|
}
|
|
95
|
+
/** The sessions whose traffic is captured on this server. */
|
|
96
|
+
sessionIds() {
|
|
97
|
+
return [...this.states.keys()];
|
|
98
|
+
}
|
|
91
99
|
start(sessionId, device, opts) {
|
|
92
100
|
return __awaiter(this, void 0, void 0, function* () {
|
|
93
101
|
var _a, _b, _c, _d;
|
|
@@ -143,7 +151,7 @@ let InterceptorService = class InterceptorService {
|
|
|
143
151
|
catch (err) {
|
|
144
152
|
this.logger.warn(`Cert install (${installMode}) failed for ${device.udid}: ${err.message}. HTTPS interception may not work.`);
|
|
145
153
|
}
|
|
146
|
-
yield
|
|
154
|
+
const previousProxy = yield this.pointPhoneAtProxy(sessionId, device.udid, host, port, routing.reverseEstablished);
|
|
147
155
|
const state = {
|
|
148
156
|
sessionId,
|
|
149
157
|
device,
|
|
@@ -155,6 +163,7 @@ let InterceptorService = class InterceptorService {
|
|
|
155
163
|
certInstalledFilename: certFilename,
|
|
156
164
|
startedAt: Date.now(),
|
|
157
165
|
reverseEstablished: routing.reverseEstablished,
|
|
166
|
+
previousProxy,
|
|
158
167
|
};
|
|
159
168
|
this.states.set(sessionId, state);
|
|
160
169
|
this.emit({ type: 'session_started', sessionId, port, host }, device.udid);
|
|
@@ -167,12 +176,7 @@ let InterceptorService = class InterceptorService {
|
|
|
167
176
|
if (!state)
|
|
168
177
|
return;
|
|
169
178
|
this.states.delete(sessionId);
|
|
170
|
-
|
|
171
|
-
yield this.getAndroidAdapter().clearProxy(state.device.udid);
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
this.logger.warn(`Clear proxy failed for ${state.device.udid}: ${err.message}`);
|
|
175
|
-
}
|
|
179
|
+
yield this.putPhoneProxyBack(state);
|
|
176
180
|
if (state.reverseEstablished) {
|
|
177
181
|
try {
|
|
178
182
|
yield this.getAndroidAdapter().removeReverse(state.device.udid, state.port);
|
|
@@ -284,15 +288,68 @@ let InterceptorService = class InterceptorService {
|
|
|
284
288
|
requests: state.buffer.list(),
|
|
285
289
|
};
|
|
286
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Points the whole phone at the session's proxy, and returns the proxy the
|
|
293
|
+
* phone had (null: none) for stop() to put back. The change is written down
|
|
294
|
+
* before it is made, so a restart after a crash can undo it
|
|
295
|
+
* (PhoneNetworkLedger, PhoneNetworkRestore).
|
|
296
|
+
*
|
|
297
|
+
* A previous proxy that is a capture an earlier run left behind (this
|
|
298
|
+
* machine, a capture port, nothing answering) is not the phone's own: it
|
|
299
|
+
* counts as none, so the end of this session doesn't put a dead proxy back.
|
|
300
|
+
*/
|
|
301
|
+
pointPhoneAtProxy(sessionId, udid, host, port, reverseEstablished) {
|
|
302
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
303
|
+
const adapter = this.getAndroidAdapter();
|
|
304
|
+
const setting = `${host}:${port}`;
|
|
305
|
+
return yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
var _a;
|
|
307
|
+
let previous = null;
|
|
308
|
+
try {
|
|
309
|
+
const read = yield adapter.readProxy(udid);
|
|
310
|
+
const leftover = read === setting ||
|
|
311
|
+
(yield (0, xenonProxy_1.isDeadCaptureProxy)(read, typedi_1.Container.get(PortAllocator_1.PortAllocator).rangeOf('proxy')));
|
|
312
|
+
if (leftover) {
|
|
313
|
+
this.logger.info(`[${sessionId}] ${udid} had a proxy left by an earlier capture (${read})`);
|
|
314
|
+
}
|
|
315
|
+
previous = (0, xenonProxy_1.isUnsetProxy)(read) || leftover ? null : read;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
this.logger.warn(`Could not read the proxy of ${udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
319
|
+
}
|
|
320
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).note(sessionId, udid, {
|
|
321
|
+
proxy: Object.assign({ set: setting, previous }, (reverseEstablished ? { reversePort: port } : {})),
|
|
322
|
+
});
|
|
323
|
+
yield adapter.setProxy(udid, host, port);
|
|
324
|
+
return previous;
|
|
325
|
+
}));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Puts back the proxy the phone had before the session. A phone that can't
|
|
330
|
+
* be reached keeps the change written down, and is put back at its next
|
|
331
|
+
* session or at this server's next start.
|
|
332
|
+
*/
|
|
333
|
+
putPhoneProxyBack(state) {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
const udid = state.device.udid;
|
|
336
|
+
yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
var _a;
|
|
338
|
+
try {
|
|
339
|
+
yield this.getAndroidAdapter().restoreProxy(udid, state.previousProxy);
|
|
340
|
+
yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(state.sessionId, 'proxy');
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
this.logger.warn(`Could not put back the proxy of ${udid} after session ${state.sessionId}: ` +
|
|
344
|
+
`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. Xenon tries again at the phone's next session and when it restarts.`);
|
|
345
|
+
}
|
|
346
|
+
}));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
287
349
|
getAndroidAdapter() {
|
|
288
350
|
if (this.androidAdapter)
|
|
289
351
|
return this.androidAdapter;
|
|
290
|
-
|
|
291
|
-
const { default: AndroidDeviceManager } = yield Promise.resolve().then(() => __importStar(require('../device-managers/AndroidDeviceManager')));
|
|
292
|
-
const mgr = typedi_1.Container.get(AndroidDeviceManager);
|
|
293
|
-
return yield mgr.getAdbForDevice(udid);
|
|
294
|
-
});
|
|
295
|
-
this.androidAdapter = new AndroidProxyAdapter_1.AndroidProxyAdapter(provider);
|
|
352
|
+
this.androidAdapter = new AndroidProxyAdapter_1.AndroidProxyAdapter(adbForPhone_1.adbForPhone);
|
|
296
353
|
return this.androidAdapter;
|
|
297
354
|
}
|
|
298
355
|
// Establishes how the device will reach the host-side MITM proxy.
|