@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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
require("reflect-metadata");
|
|
7
|
+
const chai_1 = require("chai");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
11
|
+
const plugin_1 = require("../../src/plugin");
|
|
12
|
+
const logger_1 = require("../../src/logger");
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-var-requires -- Appium's internals have no type exports */
|
|
14
|
+
const appiumSchema = require('appium/build/lib/schema/schema');
|
|
15
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
16
|
+
const xenonSchema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
|
|
17
|
+
/**
|
|
18
|
+
* XENON_JSON_LOGGING turns JSON log lines on. The plugin overwrote it with its
|
|
19
|
+
* own default, `enableJsonLogging: false`, so the variable never worked. The
|
|
20
|
+
* rule now: the option when it is set (either way), else the variable, else
|
|
21
|
+
* off. For that the option has to be absent when nobody set it, so schema.json
|
|
22
|
+
* gives it no default for Appium to fill in.
|
|
23
|
+
*/
|
|
24
|
+
describe('JSON logging', () => {
|
|
25
|
+
let savedFlag;
|
|
26
|
+
let savedEnv;
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
savedFlag = logger_1.XenonLogger.isJsonLogging;
|
|
29
|
+
savedEnv = process.env.XENON_JSON_LOGGING;
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
logger_1.XenonLogger.isJsonLogging = savedFlag;
|
|
33
|
+
if (savedEnv === undefined)
|
|
34
|
+
delete process.env.XENON_JSON_LOGGING;
|
|
35
|
+
else
|
|
36
|
+
process.env.XENON_JSON_LOGGING = savedEnv;
|
|
37
|
+
});
|
|
38
|
+
/** The logger as it stands once the plugin is built the way Appium builds it. */
|
|
39
|
+
function loggingAfterBuilding(cliArgs, env) {
|
|
40
|
+
// The environment is read when the logger loads; this is that read.
|
|
41
|
+
logger_1.XenonLogger.isJsonLogging = env === 'true';
|
|
42
|
+
new plugin_1.XenonPlugin('xenon', cliArgs);
|
|
43
|
+
return logger_1.XenonLogger.isJsonLogging;
|
|
44
|
+
}
|
|
45
|
+
it('is on when only the environment variable asks for it', () => {
|
|
46
|
+
(0, chai_1.expect)(loggingAfterBuilding({}, 'true')).to.equal(true);
|
|
47
|
+
});
|
|
48
|
+
it('is off when nothing asks for it', () => {
|
|
49
|
+
(0, chai_1.expect)(loggingAfterBuilding({})).to.equal(false);
|
|
50
|
+
});
|
|
51
|
+
it('is on when only the option asks for it', () => {
|
|
52
|
+
(0, chai_1.expect)(loggingAfterBuilding({ enableJsonLogging: true })).to.equal(true);
|
|
53
|
+
});
|
|
54
|
+
it('lets an option set to false win over the environment variable', () => {
|
|
55
|
+
(0, chai_1.expect)(loggingAfterBuilding({ enableJsonLogging: false }, 'true')).to.equal(false);
|
|
56
|
+
});
|
|
57
|
+
it('lets an option set to true win over a variable that is not "true"', () => {
|
|
58
|
+
(0, chai_1.expect)(loggingAfterBuilding({ enableJsonLogging: true }, 'false')).to.equal(true);
|
|
59
|
+
});
|
|
60
|
+
describe("Appium's defaults for an option nobody set", () => {
|
|
61
|
+
before(() => {
|
|
62
|
+
appiumSchema.resetSchema();
|
|
63
|
+
appiumSchema.registerSchema('plugin', 'xenon', xenonSchema);
|
|
64
|
+
appiumSchema.finalizeSchema();
|
|
65
|
+
});
|
|
66
|
+
after(() => appiumSchema.resetSchema());
|
|
67
|
+
it('leave enableJsonLogging absent, so the variable can be told from a choice', () => {
|
|
68
|
+
// What Appium's main does: CLI args, then the file, then schema defaults.
|
|
69
|
+
const defaults = appiumSchema.getDefaultsForSchema(false);
|
|
70
|
+
const server = lodash_1.default.defaultsDeep({}, { plugin: { xenon: { maxSessions: 8 } } }, defaults);
|
|
71
|
+
(0, chai_1.expect)(server.plugin.xenon).to.not.have.property('enableJsonLogging');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
require("reflect-metadata");
|
|
15
|
+
const chai_1 = require("chai");
|
|
16
|
+
const labSettings_1 = require("../../src/services/settings/labSettings");
|
|
17
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
18
|
+
/**
|
|
19
|
+
* The one rule for the settings the dashboard can change: a value saved there,
|
|
20
|
+
* else the plugin option the server started with, else the default.
|
|
21
|
+
*/
|
|
22
|
+
describe('lab settings', () => {
|
|
23
|
+
describe('the defaults', () => {
|
|
24
|
+
it('are the ones schema.json declares, which Appium fills in', () => {
|
|
25
|
+
(0, chai_1.expect)((0, labSettings_1.settingsDefaults)()).to.deep.equal({
|
|
26
|
+
enableSelfHealing: true,
|
|
27
|
+
healthCheckIntervalMs: 300000,
|
|
28
|
+
buildCleanupDays: 30,
|
|
29
|
+
buildCleanupMaxCount: 100,
|
|
30
|
+
buildCleanupSchedule: '0 0 * * *',
|
|
31
|
+
deleteBuildAssets: true,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('are what the plugin merges under its options', () => {
|
|
35
|
+
for (const [key, value] of Object.entries((0, labSettings_1.settingsDefaults)())) {
|
|
36
|
+
(0, chai_1.expect)(IPluginArgs_1.DefaultPluginArgs[key], key).to.equal(value);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('the settings in effect', () => {
|
|
41
|
+
it('are the defaults when nothing is saved and nothing was set at startup', () => {
|
|
42
|
+
const _a = (0, labSettings_1.effectiveSettings)({}, {}), { healthCheckSchedule } = _a, rest = __rest(_a, ["healthCheckSchedule"]);
|
|
43
|
+
(0, chai_1.expect)(rest).to.deep.equal((0, labSettings_1.settingsDefaults)());
|
|
44
|
+
(0, chai_1.expect)(healthCheckSchedule).to.equal(undefined);
|
|
45
|
+
});
|
|
46
|
+
it('take the startup option over the default', () => {
|
|
47
|
+
const s = (0, labSettings_1.effectiveSettings)({
|
|
48
|
+
buildCleanupDays: 7,
|
|
49
|
+
buildCleanupMaxCount: 20,
|
|
50
|
+
buildCleanupSchedule: '30 1 * * *',
|
|
51
|
+
deleteBuildAssets: false,
|
|
52
|
+
enableSelfHealing: false,
|
|
53
|
+
healthCheckIntervalMs: 600000,
|
|
54
|
+
healthCheckSchedule: '0 * * * *',
|
|
55
|
+
}, {});
|
|
56
|
+
(0, chai_1.expect)(s).to.deep.equal({
|
|
57
|
+
enableSelfHealing: false,
|
|
58
|
+
buildCleanupDays: 7,
|
|
59
|
+
buildCleanupMaxCount: 20,
|
|
60
|
+
buildCleanupSchedule: '30 1 * * *',
|
|
61
|
+
deleteBuildAssets: false,
|
|
62
|
+
healthCheckIntervalMs: 600000,
|
|
63
|
+
healthCheckSchedule: '0 * * * *',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('take a saved value over the startup option', () => {
|
|
67
|
+
const s = (0, labSettings_1.effectiveSettings)({
|
|
68
|
+
buildCleanupDays: 7,
|
|
69
|
+
buildCleanupMaxCount: 20,
|
|
70
|
+
buildCleanupSchedule: '30 1 * * *',
|
|
71
|
+
deleteBuildAssets: false,
|
|
72
|
+
healthCheckIntervalMs: 600000,
|
|
73
|
+
}, {
|
|
74
|
+
buildCleanupDays: 14,
|
|
75
|
+
buildCleanupMaxCount: 50,
|
|
76
|
+
buildCleanupSchedule: '0 4 * * 0',
|
|
77
|
+
deleteBuildAssets: true,
|
|
78
|
+
healthCheckIntervalMs: 120000,
|
|
79
|
+
});
|
|
80
|
+
(0, chai_1.expect)(s).to.include({
|
|
81
|
+
buildCleanupDays: 14,
|
|
82
|
+
buildCleanupMaxCount: 50,
|
|
83
|
+
buildCleanupSchedule: '0 4 * * 0',
|
|
84
|
+
deleteBuildAssets: true,
|
|
85
|
+
healthCheckIntervalMs: 120000,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it('take each setting from its own source', () => {
|
|
89
|
+
const s = (0, labSettings_1.effectiveSettings)({ buildCleanupDays: 7, buildCleanupMaxCount: 20 }, { buildCleanupDays: 14 });
|
|
90
|
+
(0, chai_1.expect)(s.buildCleanupDays).to.equal(14);
|
|
91
|
+
(0, chai_1.expect)(s.buildCleanupMaxCount).to.equal(20);
|
|
92
|
+
});
|
|
93
|
+
it('take a self-healing switch saved off over a startup on, and saved on over a startup off', () => {
|
|
94
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: true }, { enableSelfHealing: false })
|
|
95
|
+
.enableSelfHealing).to.equal(false);
|
|
96
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: false }, { enableSelfHealing: true })
|
|
97
|
+
.enableSelfHealing).to.equal(true);
|
|
98
|
+
});
|
|
99
|
+
it('take the self-healing switch from the startup option, else on', () => {
|
|
100
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: false }, {}).enableSelfHealing).to.equal(false);
|
|
101
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({}, {}).enableSelfHealing).to.equal(true);
|
|
102
|
+
});
|
|
103
|
+
it('skip a self-healing switch that is not a boolean, taking the next source', () => {
|
|
104
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: false }, { enableSelfHealing: 'true' })
|
|
105
|
+
.enableSelfHealing).to.equal(false);
|
|
106
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: 'false' }, {}).enableSelfHealing).to.equal(true);
|
|
107
|
+
});
|
|
108
|
+
it('keep a deleteBuildAssets saved as false over a startup true', () => {
|
|
109
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ deleteBuildAssets: true }, { deleteBuildAssets: false })
|
|
110
|
+
.deleteBuildAssets).to.equal(false);
|
|
111
|
+
});
|
|
112
|
+
it('skip a saved value that could not work, taking the next source', () => {
|
|
113
|
+
const s = (0, labSettings_1.effectiveSettings)({ buildCleanupDays: 7, buildCleanupMaxCount: 20, buildCleanupSchedule: '30 1 * * *' }, {
|
|
114
|
+
buildCleanupDays: 0,
|
|
115
|
+
buildCleanupMaxCount: NaN,
|
|
116
|
+
buildCleanupSchedule: 'nightly',
|
|
117
|
+
healthCheckIntervalMs: -5,
|
|
118
|
+
});
|
|
119
|
+
(0, chai_1.expect)(s).to.include({
|
|
120
|
+
buildCleanupDays: 7,
|
|
121
|
+
buildCleanupMaxCount: 20,
|
|
122
|
+
buildCleanupSchedule: '30 1 * * *',
|
|
123
|
+
healthCheckIntervalMs: 300000,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it('skip a startup value that could not work, taking the default', () => {
|
|
127
|
+
const s = (0, labSettings_1.effectiveSettings)({ buildCleanupDays: 0, buildCleanupSchedule: 'nightly' }, {});
|
|
128
|
+
(0, chai_1.expect)(s.buildCleanupDays).to.equal(30);
|
|
129
|
+
(0, chai_1.expect)(s.buildCleanupSchedule).to.equal('0 0 * * *');
|
|
130
|
+
});
|
|
131
|
+
it('let a health schedule that was cleared in the dashboard stay cleared', () => {
|
|
132
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ healthCheckSchedule: '0 * * * *' }, { healthCheckSchedule: '' })
|
|
133
|
+
.healthCheckSchedule).to.equal(undefined);
|
|
134
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ healthCheckSchedule: '0 * * * *' }, {}).healthCheckSchedule).to.equal('0 * * * *');
|
|
135
|
+
(0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ healthCheckSchedule: '0 * * * *' }, { healthCheckSchedule: '*/10 * * * *' }).healthCheckSchedule).to.equal('*/10 * * * *');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('whether self-healing runs (what the command interceptor asks)', () => {
|
|
139
|
+
it('is the saved value, else the startup option, else on', () => {
|
|
140
|
+
(0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(true, false)).to.equal(false);
|
|
141
|
+
(0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(false, true)).to.equal(true);
|
|
142
|
+
(0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(false, undefined)).to.equal(false);
|
|
143
|
+
(0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(undefined, undefined)).to.equal(true);
|
|
144
|
+
});
|
|
145
|
+
it('agrees with the settings in effect for every combination', () => {
|
|
146
|
+
for (const startup of [true, false, undefined, 'false', null]) {
|
|
147
|
+
for (const saved of [true, false, undefined]) {
|
|
148
|
+
(0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(startup, saved), `startup ${String(startup)}, saved ${String(saved)}`).to.equal((0, labSettings_1.effectiveSettings)({ enableSelfHealing: startup }, { enableSelfHealing: saved })
|
|
149
|
+
.enableSelfHealing);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('a cron expression', () => {
|
|
155
|
+
for (const ok of [
|
|
156
|
+
'0 0 * * *',
|
|
157
|
+
'*/15 * * * *',
|
|
158
|
+
'0 2 * * 0',
|
|
159
|
+
'0 */12 * * *',
|
|
160
|
+
'30 1 1 jan *',
|
|
161
|
+
'*/5 * * * * *',
|
|
162
|
+
]) {
|
|
163
|
+
it(`accepts ${JSON.stringify(ok)}`, () => (0, chai_1.expect)((0, labSettings_1.isValidCron)(ok)).to.equal(true));
|
|
164
|
+
}
|
|
165
|
+
for (const bad of [
|
|
166
|
+
'',
|
|
167
|
+
'nightly',
|
|
168
|
+
'0 0 * *',
|
|
169
|
+
'60 * * * *',
|
|
170
|
+
'0 0 31 2 *',
|
|
171
|
+
'* * * * * * *',
|
|
172
|
+
'2030-01-01',
|
|
173
|
+
'Jan 1 2030 10:00:00',
|
|
174
|
+
null,
|
|
175
|
+
42,
|
|
176
|
+
undefined,
|
|
177
|
+
]) {
|
|
178
|
+
it(`refuses ${JSON.stringify(bad)}`, () => (0, chai_1.expect)((0, labSettings_1.isValidCron)(bad)).to.equal(false));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
describe('an update', () => {
|
|
182
|
+
it('is accepted when every cleanup field is sound, or none is sent', () => {
|
|
183
|
+
(0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({})).to.equal(null);
|
|
184
|
+
(0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({
|
|
185
|
+
buildCleanupDays: 14,
|
|
186
|
+
buildCleanupMaxCount: 50,
|
|
187
|
+
buildCleanupSchedule: '0 2 * * *',
|
|
188
|
+
deleteBuildAssets: false,
|
|
189
|
+
})).to.equal(null);
|
|
190
|
+
});
|
|
191
|
+
it('is not held to a rule for the settings the cleanup job does not act on', () => {
|
|
192
|
+
(0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({
|
|
193
|
+
healthCheckIntervalMs: 300000,
|
|
194
|
+
aiProvider: 'gemini',
|
|
195
|
+
})).to.equal(null);
|
|
196
|
+
});
|
|
197
|
+
it('accepts the self-healing switch as true or false, and refuses anything else', () => {
|
|
198
|
+
var _a;
|
|
199
|
+
(0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: true })).to.equal(null);
|
|
200
|
+
(0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: false })).to.equal(null);
|
|
201
|
+
for (const bad of ['true', 0, null, {}]) {
|
|
202
|
+
(0, chai_1.expect)((_a = (0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: bad })) === null || _a === void 0 ? void 0 : _a.field).to.equal('enableSelfHealing');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
it('names the field a refused value was sent for', () => {
|
|
206
|
+
var _a, _b, _c, _d, _e;
|
|
207
|
+
(0, chai_1.expect)((_a = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupDays: 0 })) === null || _a === void 0 ? void 0 : _a.field).to.equal('buildCleanupDays');
|
|
208
|
+
(0, chai_1.expect)((_b = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupMaxCount: 1.5 })) === null || _b === void 0 ? void 0 : _b.field).to.equal('buildCleanupMaxCount');
|
|
209
|
+
(0, chai_1.expect)((_c = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupSchedule: 'x' })) === null || _c === void 0 ? void 0 : _c.field).to.equal('buildCleanupSchedule');
|
|
210
|
+
(0, chai_1.expect)((_d = (0, labSettings_1.validateSettingsUpdate)({ deleteBuildAssets: 1 })) === null || _d === void 0 ? void 0 : _d.field).to.equal('deleteBuildAssets');
|
|
211
|
+
(0, chai_1.expect)((_e = (0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: 'yes' })) === null || _e === void 0 ? void 0 : _e.field).to.equal('enableSelfHealing');
|
|
212
|
+
});
|
|
213
|
+
it('treats a field sent as null as sent', () => {
|
|
214
|
+
var _a;
|
|
215
|
+
(0, chai_1.expect)((_a = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupDays: null })) === null || _a === void 0 ? void 0 : _a.field).to.equal('buildCleanupDays');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -26,14 +26,31 @@ describe('canOverrideLease — who may use a lease they did not create', () => {
|
|
|
26
26
|
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'api-key', scopes: 'admin', user: null })).to.equal(false);
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
// A session token carries the scopes of the credential that minted it, and
|
|
30
|
+
// is judged by them like a key. By role alone, an ADMIN's key without the
|
|
31
|
+
// admin scope minted itself a token that could override.
|
|
32
|
+
describe("a session token (judged by its scopes and its user's role now)", () => {
|
|
33
|
+
// resolveActor's rule (a SUPER_ADMIN, or a credential with `admin`), on
|
|
34
|
+
// the user's role now: the scope is fixed at mint, so an ADMIN's token
|
|
35
|
+
// also needs the role to still be ADMIN, which notices a demotion.
|
|
36
|
+
it("is allowed for an ADMIN's or SUPER_ADMIN's token carrying the admin scope", () => {
|
|
37
|
+
for (const role of ['ADMIN', 'SUPER_ADMIN']) {
|
|
38
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin,sessions', user: user(role) })).to.equal(true);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// As their key pair overrides, and their Bearer token in per-command auth.
|
|
42
|
+
it("is allowed for a SUPER_ADMIN's token without the admin scope", () => {
|
|
43
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('SUPER_ADMIN') })).to.equal(true);
|
|
44
|
+
});
|
|
45
|
+
it('is refused for a token with the admin scope whose user is now a member', () => {
|
|
46
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin,sessions', user: user('MEMBER') })).to.equal(false);
|
|
47
|
+
});
|
|
48
|
+
it("is refused for an ADMIN's token without the admin scope", () => {
|
|
49
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('ADMIN') })).to.equal(false);
|
|
33
50
|
});
|
|
34
51
|
it('is refused for a member, or a user who is not active', () => {
|
|
35
|
-
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: user('MEMBER') })).to.equal(false);
|
|
36
|
-
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', user: null })).to.equal(false);
|
|
52
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'sessions', user: user('MEMBER') })).to.equal(false);
|
|
53
|
+
(0, chai_1.expect)((0, leaseOverride_1.canOverrideLease)({ kind: 'session-token', scopes: 'admin', user: null })).to.equal(false);
|
|
37
54
|
});
|
|
38
55
|
});
|
|
39
56
|
it('is allowed with auth disabled, where every caller is a SUPER_ADMIN', () => {
|
|
@@ -83,8 +83,11 @@ const sessionCaps = (xenonOptions, extra = {}) => ({
|
|
|
83
83
|
const key = (accessKey) => ({ accessKey, token: 'tk' });
|
|
84
84
|
const ownerKey = key('ak_owner');
|
|
85
85
|
const otherKey = key('ak_other');
|
|
86
|
-
// A session token names its user in `sub
|
|
87
|
-
|
|
86
|
+
// A session token names its user in `sub` and carries the scopes of the
|
|
87
|
+
// credential that minted it; the fake verifier echoes both back.
|
|
88
|
+
const sessionToken = (sub, scopes = 'sessions') => ({
|
|
89
|
+
sessionToken: `jwt:${sub}|${scopes}`,
|
|
90
|
+
});
|
|
88
91
|
const keys = {
|
|
89
92
|
ak_owner: { id: 'key_owner', userId: 'usr_owner', scopes: 'sessions', teamId: null },
|
|
90
93
|
ak_other: { id: 'key_other', userId: 'usr_other', scopes: 'sessions', teamId: null },
|
|
@@ -174,7 +177,10 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
174
177
|
findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return roles[id] ? { id, role: roles[id], status: 'ACTIVE' } : null; }),
|
|
175
178
|
});
|
|
176
179
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
177
|
-
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
181
|
+
const [sub, scopes] = t.replace(/^jwt:/, '').split('|');
|
|
182
|
+
return { sub, teamId: null, scopes };
|
|
183
|
+
}),
|
|
178
184
|
});
|
|
179
185
|
teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
180
186
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
@@ -331,8 +337,14 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
331
337
|
it("is allowed to a SUPER_ADMIN's key", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
332
338
|
(0, chai_1.expect)(yield allowed(sessionCaps(key('ak_super')))).to.equal(true);
|
|
333
339
|
}));
|
|
340
|
+
// A session token minted from the dashboard carries the admin's admin
|
|
341
|
+
// scope; one minted with an ADMIN's narrower key doesn't, and overrides
|
|
342
|
+
// no more than that key does.
|
|
334
343
|
it('is allowed to an ADMIN presenting a session token, as on the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
335
|
-
(0, chai_1.expect)(yield allowed(sessionCaps(sessionToken('usr_admin')))).to.equal(true);
|
|
344
|
+
(0, chai_1.expect)(yield allowed(sessionCaps(sessionToken('usr_admin', 'admin,sessions')))).to.equal(true);
|
|
345
|
+
}));
|
|
346
|
+
it("is refused to an ADMIN's session token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
347
|
+
(0, chai_1.expect)(yield refusal(sessionCaps(sessionToken('usr_admin', 'sessions')))).to.equal(REFUSED);
|
|
336
348
|
}));
|
|
337
349
|
it('is refused to a member presenting a session token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
338
350
|
(0, chai_1.expect)(yield refusal(sessionCaps(sessionToken('usr_other')))).to.equal(REFUSED);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = __importDefault(require("chai"));
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const DeviceUtils = __importStar(require("../../src/device-utils"));
|
|
52
|
+
const db_1 = require("../../src/data-service/db");
|
|
53
|
+
const device_service_1 = require("../../src/data-service/device-service");
|
|
54
|
+
const deviceClaims_1 = require("../../src/data-service/deviceClaims");
|
|
55
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
56
|
+
const test_container_1 = require("../helpers/test-container");
|
|
57
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
58
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
59
|
+
const expect = chai_1.default.expect;
|
|
60
|
+
/**
|
|
61
|
+
* `maxSessions` holds new sessions back once that many Appium sessions run.
|
|
62
|
+
*
|
|
63
|
+
* It compared the number of busy phones with `===`, so once the count was past
|
|
64
|
+
* the limit nothing was held back any more, and it counted every busy phone: a
|
|
65
|
+
* live preview, a recording, or an SDK lease nobody had started a session on
|
|
66
|
+
* used up a slot that no Appium session was using. What it counts now is the
|
|
67
|
+
* phones running a session, or being given one.
|
|
68
|
+
*/
|
|
69
|
+
describe('maxSessions', () => {
|
|
70
|
+
describe('which phones count', () => {
|
|
71
|
+
const base = { busy: true };
|
|
72
|
+
it('counts a session being created (a claim with no session id yet)', () => {
|
|
73
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { claimedAt: Date.now() }))).to.equal(true);
|
|
74
|
+
});
|
|
75
|
+
it('counts a session that is running', () => {
|
|
76
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { claimedAt: 1, claimSessionId: 'sess-1', session_id: 'sess-1' }))).to.equal(true);
|
|
77
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { session_id: 'sess-1' }))).to.equal(true);
|
|
78
|
+
});
|
|
79
|
+
it('does not count a live preview or a recording', () => {
|
|
80
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { session_id: 'manual_u1_phone-1' }))).to.equal(false);
|
|
81
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { session_id: 'manual_phone-1' }))).to.equal(false);
|
|
82
|
+
});
|
|
83
|
+
it('does not count a phone an SDK lease holds that has no session on it', () => {
|
|
84
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign({}, base))).to.equal(false);
|
|
85
|
+
});
|
|
86
|
+
it('counts a session started on a leased phone', () => {
|
|
87
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { claimSessionId: 'sess-2', claimedAt: 1 }))).to.equal(true);
|
|
88
|
+
});
|
|
89
|
+
it('does not count a phone that is not busy', () => {
|
|
90
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)({ busy: false })).to.equal(false);
|
|
91
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)({ busy: false, session_id: 'sess-1' })).to.equal(false);
|
|
92
|
+
});
|
|
93
|
+
it("counts a node's phone the node reports busy, unless the node says it is a preview", () => {
|
|
94
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { nodeBusy: true }))).to.equal(true);
|
|
95
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { nodeBusy: true, nodeHold: 'manual_u1_phone-1' }))).to.equal(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('the limit itself', () => {
|
|
99
|
+
it('is the setting when it is a positive number', () => {
|
|
100
|
+
expect((0, deviceClaims_1.sessionCap)(8)).to.equal(8);
|
|
101
|
+
expect((0, deviceClaims_1.sessionCap)(1)).to.equal(1);
|
|
102
|
+
});
|
|
103
|
+
it('is no limit when it is unset, zero, negative or not a number', () => {
|
|
104
|
+
for (const value of [undefined, null, 0, -1, NaN, Infinity, '8']) {
|
|
105
|
+
expect((0, deviceClaims_1.sessionCap)(value), String(value)).to.equal(undefined);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('allocation', () => {
|
|
110
|
+
(0, loki_stores_1.useLokiStores)();
|
|
111
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
112
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
113
|
+
const HOST = 'http://127.0.0.1:4723';
|
|
114
|
+
const pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, {
|
|
115
|
+
iosDeviceType: 'both',
|
|
116
|
+
androidDeviceType: 'both',
|
|
117
|
+
});
|
|
118
|
+
const phone = (udid, over = {}) => (Object.assign({ udid, name: udid, platform: 'android', sdk: '13', host: HOST, busy: false, offline: false, userBlocked: false, state: 'device', deviceType: 'emulator', realDevice: false, systemPort: 57000, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
|
|
119
|
+
// New each time: allocation pins the phone it picked into the caps it is given.
|
|
120
|
+
const caps = () => ({
|
|
121
|
+
alwaysMatch: { platformName: 'android' },
|
|
122
|
+
firstMatch: [{}],
|
|
123
|
+
});
|
|
124
|
+
const row = (udid) => __awaiter(void 0, void 0, void 0, function* () { return (yield db_1.XenonDatabase.DeviceModel).findOne({ udid }); });
|
|
125
|
+
/** Asks for a phone, waiting `ms` at most. Resolves to the udid, or null when held back. */
|
|
126
|
+
const ask = (ms = 400) => DeviceUtils.allocateDeviceForSession(caps(), ms, 50, pluginArgs).then((d) => d.udid, () => null);
|
|
127
|
+
const withCap = (maxSessions) => (0, test_container_1.createTestXenonManager)(Object.assign({}, pluginArgs, { maxSessions, platform: 'android' }));
|
|
128
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
yield (0, test_container_1.resetTestContainer)();
|
|
130
|
+
yield (0, test_container_1.setupTestContainer)();
|
|
131
|
+
}));
|
|
132
|
+
after(() => (0, test_container_1.resetTestContainer)());
|
|
133
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
(yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
|
|
135
|
+
yield scratch.db.lease.deleteMany({});
|
|
136
|
+
sandbox.stub(DeviceUtils, 'setUtilizationTime').resolves();
|
|
137
|
+
}));
|
|
138
|
+
afterEach(() => sandbox.restore());
|
|
139
|
+
const running = (n) => Array.from({ length: n }, (_, i) => phone(`running-${i}`, {
|
|
140
|
+
busy: true,
|
|
141
|
+
session_id: `sess-${i}`,
|
|
142
|
+
claimSessionId: `sess-${i}`,
|
|
143
|
+
claimedAt: 1,
|
|
144
|
+
}));
|
|
145
|
+
it('allocates while fewer sessions run than the limit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
withCap(2);
|
|
147
|
+
yield (0, device_service_1.addNewDevice)([...running(1), phone('free-1')]);
|
|
148
|
+
expect(yield ask()).to.equal('free-1');
|
|
149
|
+
}));
|
|
150
|
+
it('holds a session back at the limit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
withCap(2);
|
|
152
|
+
yield (0, device_service_1.addNewDevice)([...running(2), phone('free-1')]);
|
|
153
|
+
expect(yield ask()).to.equal(null);
|
|
154
|
+
expect((yield row('free-1')).busy).to.equal(false);
|
|
155
|
+
}));
|
|
156
|
+
it('still holds it back when more sessions run than the limit, which `===` let through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
|
+
withCap(2);
|
|
158
|
+
yield (0, device_service_1.addNewDevice)([...running(3), phone('free-1')]);
|
|
159
|
+
expect(yield ask()).to.equal(null);
|
|
160
|
+
expect((yield row('free-1')).busy).to.equal(false);
|
|
161
|
+
}));
|
|
162
|
+
it('gives the phone once a session ends', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
163
|
+
withCap(2);
|
|
164
|
+
yield (0, device_service_1.addNewDevice)([...running(2), phone('free-1')]);
|
|
165
|
+
const waiting = ask(2000);
|
|
166
|
+
yield new Promise((r) => setTimeout(r, 150));
|
|
167
|
+
(yield db_1.XenonDatabase.DeviceModel)
|
|
168
|
+
.chain()
|
|
169
|
+
.find({ udid: 'running-0' })
|
|
170
|
+
.update((d) => Object.assign(d, {
|
|
171
|
+
busy: false,
|
|
172
|
+
session_id: null,
|
|
173
|
+
claimSessionId: null,
|
|
174
|
+
claimedAt: null,
|
|
175
|
+
}));
|
|
176
|
+
expect(yield waiting).to.be.oneOf(['free-1', 'running-0']);
|
|
177
|
+
}));
|
|
178
|
+
it('does not count live previews: they are not sessions', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
|
+
withCap(2);
|
|
180
|
+
yield (0, device_service_1.addNewDevice)([
|
|
181
|
+
phone('preview-1', { busy: true, session_id: 'manual_u1_preview-1' }),
|
|
182
|
+
phone('preview-2', { busy: true, session_id: 'manual_u1_preview-2' }),
|
|
183
|
+
phone('free-1'),
|
|
184
|
+
]);
|
|
185
|
+
expect(yield ask()).to.equal('free-1');
|
|
186
|
+
}));
|
|
187
|
+
it('does not count a leased phone no session is running on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
withCap(1);
|
|
189
|
+
yield (0, device_service_1.addNewDevice)([phone('leased-1', { busy: true }), phone('free-1')]);
|
|
190
|
+
yield scratch.db.lease.create({
|
|
191
|
+
data: {
|
|
192
|
+
tokenHash: 'h',
|
|
193
|
+
deviceUdid: 'leased-1',
|
|
194
|
+
deviceHost: HOST,
|
|
195
|
+
actorId: 'key_owner',
|
|
196
|
+
status: 'active',
|
|
197
|
+
expiresAt: Date.now() + 600000,
|
|
198
|
+
lastHeartbeatAt: Date.now(),
|
|
199
|
+
allocatedPorts: '{}',
|
|
200
|
+
capabilityBag: '{}',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
expect(yield ask()).to.equal('free-1');
|
|
204
|
+
}));
|
|
205
|
+
it('counts a session started on a leased phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
withCap(1);
|
|
207
|
+
yield (0, device_service_1.addNewDevice)([
|
|
208
|
+
phone('leased-1', {
|
|
209
|
+
busy: true,
|
|
210
|
+
session_id: 'sess-l',
|
|
211
|
+
claimSessionId: 'sess-l',
|
|
212
|
+
claimedAt: 1,
|
|
213
|
+
}),
|
|
214
|
+
phone('free-1'),
|
|
215
|
+
]);
|
|
216
|
+
expect(yield ask()).to.equal(null);
|
|
217
|
+
}));
|
|
218
|
+
it('counts a session still being created', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
withCap(1);
|
|
220
|
+
yield (0, device_service_1.addNewDevice)([
|
|
221
|
+
phone('pending-1', { busy: true, claimedAt: Date.now() }),
|
|
222
|
+
phone('free-1'),
|
|
223
|
+
]);
|
|
224
|
+
expect(yield ask()).to.equal(null);
|
|
225
|
+
}));
|
|
226
|
+
it('holds back exactly the sessions over the limit when many ask at once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
|
+
withCap(2);
|
|
228
|
+
yield (0, device_service_1.addNewDevice)(Array.from({ length: 6 }, (_, i) => phone(`free-${i}`)));
|
|
229
|
+
const got = yield Promise.all(Array.from({ length: 5 }, () => ask(300)));
|
|
230
|
+
const given = got.filter((udid) => udid !== null);
|
|
231
|
+
expect(given, JSON.stringify(got)).to.have.length(2);
|
|
232
|
+
expect(new Set(given).size).to.equal(2);
|
|
233
|
+
}));
|
|
234
|
+
it('has no limit when the setting is not a positive number', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
235
|
+
withCap(0);
|
|
236
|
+
yield (0, device_service_1.addNewDevice)([...running(3), phone('free-1')]);
|
|
237
|
+
expect(yield ask()).to.equal('free-1');
|
|
238
|
+
}));
|
|
239
|
+
});
|
|
240
|
+
});
|