@xenon-device-management/xenon 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +57 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
21
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
22
|
+
const config_1 = require("../../src/config");
|
|
23
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
24
|
+
const aiEngineSettings_1 = require("../../src/services/settings/aiEngineSettings");
|
|
25
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
26
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
27
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
28
|
+
/**
|
|
29
|
+
* The AI engine page's choice of provider was kept in memory only: `POST
|
|
30
|
+
* /config` changed the running server's settings and saved nothing, so a
|
|
31
|
+
* restart went back to the provider the server was started with. The model and
|
|
32
|
+
* base URL the same route accepts went the same way.
|
|
33
|
+
*
|
|
34
|
+
* They follow the other lab settings now: a value saved in the dashboard wins
|
|
35
|
+
* over the plugin option or environment variable the server started with, which
|
|
36
|
+
* wins over the default, and it applies to the next AI call without a restart.
|
|
37
|
+
* API keys are never saved or sent: they stay in the server's environment.
|
|
38
|
+
*/
|
|
39
|
+
describe('the AI engine settings', () => {
|
|
40
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
41
|
+
const AI_FIELDS = [
|
|
42
|
+
'aiProvider',
|
|
43
|
+
'aiModel',
|
|
44
|
+
'aiBaseUrl',
|
|
45
|
+
'geminiModel',
|
|
46
|
+
'openaiModel',
|
|
47
|
+
'anthropicModel',
|
|
48
|
+
'ollamaModel',
|
|
49
|
+
'geminiApiKey',
|
|
50
|
+
'openaiApiKey',
|
|
51
|
+
'anthropicApiKey',
|
|
52
|
+
];
|
|
53
|
+
let startedWith;
|
|
54
|
+
let restoreContainer;
|
|
55
|
+
/** The AI settings the server was started with (its option, else its environment). */
|
|
56
|
+
const startup = (over = {}) => {
|
|
57
|
+
const blank = {};
|
|
58
|
+
for (const field of AI_FIELDS)
|
|
59
|
+
blank[field] = undefined;
|
|
60
|
+
Object.assign(config_1.config, blank, { aiProvider: 'gemini' }, over);
|
|
61
|
+
};
|
|
62
|
+
/** A server process starting: nothing in memory, the settings read from the database. */
|
|
63
|
+
const restart = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
|
+
typedi_1.Container.set(aiEngineSettings_1.AiEngineSettings, new aiEngineSettings_1.AiEngineSettings());
|
|
65
|
+
yield typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).load();
|
|
66
|
+
});
|
|
67
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
|
+
startedWith = {};
|
|
69
|
+
for (const field of AI_FIELDS)
|
|
70
|
+
startedWith[field] = config_1.config[field];
|
|
71
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(aiEngineSettings_1.AiEngineSettings);
|
|
72
|
+
// A service of its own, as at boot, so nothing saved leaks in from another spec.
|
|
73
|
+
typedi_1.Container.set(aiEngineSettings_1.AiEngineSettings, new aiEngineSettings_1.AiEngineSettings());
|
|
74
|
+
yield scratch.db.webConfig.deleteMany({});
|
|
75
|
+
}));
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
Object.assign(config_1.config, startedWith);
|
|
78
|
+
restoreContainer();
|
|
79
|
+
sinon_1.default.restore();
|
|
80
|
+
});
|
|
81
|
+
function app() {
|
|
82
|
+
const a = (0, express_1.default)();
|
|
83
|
+
a.use(express_1.default.json());
|
|
84
|
+
a.use((req, _res, next) => {
|
|
85
|
+
req.auth = {
|
|
86
|
+
kind: 'user-session',
|
|
87
|
+
userId: 'usr_x',
|
|
88
|
+
role: 'SUPER_ADMIN',
|
|
89
|
+
scopes: 'admin,devices,sessions,read',
|
|
90
|
+
rateLimit: 1000,
|
|
91
|
+
};
|
|
92
|
+
next();
|
|
93
|
+
});
|
|
94
|
+
dashboard_1.default.register(a);
|
|
95
|
+
return a;
|
|
96
|
+
}
|
|
97
|
+
const save = (body) => (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
|
|
98
|
+
const read = () => (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
99
|
+
describe('the provider chosen on the page', () => {
|
|
100
|
+
it('is saved, and shown by GET /config', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
startup({ anthropicApiKey: 'sk-ant-test' });
|
|
102
|
+
const saved = yield save({ aiProvider: 'anthropic' });
|
|
103
|
+
(0, chai_1.expect)(saved.status).to.equal(200);
|
|
104
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
|
|
105
|
+
aiProvider: 'anthropic',
|
|
106
|
+
});
|
|
107
|
+
(0, chai_1.expect)((yield read()).body.aiProvider).to.equal('anthropic');
|
|
108
|
+
}));
|
|
109
|
+
it('applies to the running server at once, with no restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
startup();
|
|
111
|
+
yield save({ aiProvider: 'openai' });
|
|
112
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
|
|
113
|
+
}));
|
|
114
|
+
it('is still the provider after a restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
startup();
|
|
116
|
+
yield save({ aiProvider: 'ollama' });
|
|
117
|
+
startup(); // the process starts again with its option or environment: gemini
|
|
118
|
+
yield restart();
|
|
119
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('ollama');
|
|
120
|
+
(0, chai_1.expect)((yield read()).body.aiProvider).to.equal('ollama');
|
|
121
|
+
}));
|
|
122
|
+
it('wins over the provider the server was started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
startup({ aiProvider: 'openai' });
|
|
124
|
+
yield save({ aiProvider: 'anthropic' });
|
|
125
|
+
startup({ aiProvider: 'openai' });
|
|
126
|
+
yield restart();
|
|
127
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('anthropic');
|
|
128
|
+
}));
|
|
129
|
+
it('leaves the started-with provider in charge when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
startup({ aiProvider: 'anthropic' });
|
|
131
|
+
yield restart();
|
|
132
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('anthropic');
|
|
133
|
+
(0, chai_1.expect)((yield read()).body.aiProvider).to.equal('anthropic');
|
|
134
|
+
}));
|
|
135
|
+
it('goes back to the started-with provider when the saved one is cleared', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
136
|
+
startup({ aiProvider: 'openai' });
|
|
137
|
+
yield save({ aiProvider: 'anthropic' });
|
|
138
|
+
yield save({ aiProvider: '' });
|
|
139
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
|
|
140
|
+
startup({ aiProvider: 'openai' });
|
|
141
|
+
yield restart();
|
|
142
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
|
|
143
|
+
}));
|
|
144
|
+
it('is saved without touching the other settings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
|
+
startup();
|
|
146
|
+
yield save({ buildCleanupDays: 7 });
|
|
147
|
+
yield save({ aiProvider: 'openai' });
|
|
148
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
|
|
149
|
+
buildCleanupDays: 7,
|
|
150
|
+
aiProvider: 'openai',
|
|
151
|
+
});
|
|
152
|
+
}));
|
|
153
|
+
});
|
|
154
|
+
describe('the models and base URL', () => {
|
|
155
|
+
it('are saved, applied at once, and kept across a restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
156
|
+
startup();
|
|
157
|
+
const saved = yield save({
|
|
158
|
+
ollamaModel: 'llava',
|
|
159
|
+
aiBaseUrl: 'http://gpu-box.lab:11434',
|
|
160
|
+
geminiModel: 'gemini-2.5-pro',
|
|
161
|
+
openaiModel: 'gpt-4.1',
|
|
162
|
+
anthropicModel: 'claude-opus-4-1',
|
|
163
|
+
aiModel: 'llava:13b',
|
|
164
|
+
});
|
|
165
|
+
(0, chai_1.expect)(saved.status).to.equal(200);
|
|
166
|
+
(0, chai_1.expect)(config_1.config.ollamaModel).to.equal('llava');
|
|
167
|
+
(0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('http://gpu-box.lab:11434');
|
|
168
|
+
startup();
|
|
169
|
+
yield restart();
|
|
170
|
+
(0, chai_1.expect)(config_1.config).to.include({
|
|
171
|
+
ollamaModel: 'llava',
|
|
172
|
+
aiBaseUrl: 'http://gpu-box.lab:11434',
|
|
173
|
+
geminiModel: 'gemini-2.5-pro',
|
|
174
|
+
openaiModel: 'gpt-4.1',
|
|
175
|
+
anthropicModel: 'claude-opus-4-1',
|
|
176
|
+
aiModel: 'llava:13b',
|
|
177
|
+
});
|
|
178
|
+
const shown = (yield read()).body;
|
|
179
|
+
(0, chai_1.expect)(shown).to.include({ ollamaModel: 'llava', aiBaseUrl: 'http://gpu-box.lab:11434' });
|
|
180
|
+
}));
|
|
181
|
+
it('win over the values the server was started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
startup({ ollamaModel: 'llama3', aiBaseUrl: 'http://localhost:11434' });
|
|
183
|
+
yield save({ ollamaModel: 'llava', aiBaseUrl: 'http://gpu-box.lab:11434' });
|
|
184
|
+
startup({ ollamaModel: 'llama3', aiBaseUrl: 'http://localhost:11434' });
|
|
185
|
+
yield restart();
|
|
186
|
+
(0, chai_1.expect)(config_1.config.ollamaModel).to.equal('llava');
|
|
187
|
+
(0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('http://gpu-box.lab:11434');
|
|
188
|
+
}));
|
|
189
|
+
it('go back to the started-with values when cleared with an empty value', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
190
|
+
startup({ ollamaModel: 'llama3', aiBaseUrl: 'http://localhost:11434' });
|
|
191
|
+
yield save({ ollamaModel: 'llava', aiBaseUrl: 'http://gpu-box.lab:11434' });
|
|
192
|
+
yield save({ ollamaModel: '', aiBaseUrl: '' });
|
|
193
|
+
(0, chai_1.expect)(config_1.config.ollamaModel).to.equal('llama3');
|
|
194
|
+
(0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('http://localhost:11434');
|
|
195
|
+
}));
|
|
196
|
+
it('go back to no value at all when cleared and the server was started with none', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
197
|
+
startup();
|
|
198
|
+
yield save({ geminiModel: 'gemini-2.5-pro' });
|
|
199
|
+
yield save({ geminiModel: '' });
|
|
200
|
+
(0, chai_1.expect)(config_1.config.geminiModel).to.equal(undefined);
|
|
201
|
+
(0, chai_1.expect)((yield read()).body.geminiModel).to.equal(undefined);
|
|
202
|
+
}));
|
|
203
|
+
it('take surrounding spaces off a model name', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
204
|
+
startup();
|
|
205
|
+
yield save({ openaiModel: ' gpt-4.1 ' });
|
|
206
|
+
(0, chai_1.expect)(config_1.config.openaiModel).to.equal('gpt-4.1');
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
describe('what is refused, with a 400 naming the setting and nothing saved', () => {
|
|
210
|
+
var _a;
|
|
211
|
+
const refused = [
|
|
212
|
+
['aiProvider', 'gpt'],
|
|
213
|
+
['aiProvider', 'Gemini'],
|
|
214
|
+
['aiProvider', 1],
|
|
215
|
+
['aiProvider', null],
|
|
216
|
+
['ollamaModel', 7],
|
|
217
|
+
['ollamaModel', 'llama 3'],
|
|
218
|
+
['geminiModel', 'a'.repeat(201)],
|
|
219
|
+
['aiModel', 'gpt-4o\nx'],
|
|
220
|
+
['aiBaseUrl', 'localhost:11434'],
|
|
221
|
+
['aiBaseUrl', 'ftp://models.lab/'],
|
|
222
|
+
['aiBaseUrl', 'not a url'],
|
|
223
|
+
// A key in the address would be a key saved in the database.
|
|
224
|
+
['aiBaseUrl', 'https://user:secret@llm.lab/v1'],
|
|
225
|
+
['aiBaseUrl', 'https://llm.lab/v1?key=secret'],
|
|
226
|
+
];
|
|
227
|
+
for (const [field, value] of refused) {
|
|
228
|
+
it(`${field}: ${(_a = JSON.stringify(value)) === null || _a === void 0 ? void 0 : _a.slice(0, 40)}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
229
|
+
startup();
|
|
230
|
+
const answer = yield save({ [field]: value, buildCleanupDays: 9 });
|
|
231
|
+
(0, chai_1.expect)(answer.status).to.equal(400);
|
|
232
|
+
(0, chai_1.expect)(answer.body).to.include({ error: 'invalid_setting', field });
|
|
233
|
+
(0, chai_1.expect)(answer.body.message).to.be.a('string').and.not.equal('');
|
|
234
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
|
|
235
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('gemini');
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
describe('API keys', () => {
|
|
240
|
+
it('are never saved, and never change the running server', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
241
|
+
startup({ geminiApiKey: 'server-key' });
|
|
242
|
+
const answer = yield save({
|
|
243
|
+
aiProvider: 'gemini',
|
|
244
|
+
geminiApiKey: 'page-key',
|
|
245
|
+
openaiApiKey: 'page-key',
|
|
246
|
+
anthropicApiKey: 'page-key',
|
|
247
|
+
});
|
|
248
|
+
(0, chai_1.expect)(answer.status).to.equal(200);
|
|
249
|
+
(0, chai_1.expect)(config_1.config.geminiApiKey).to.equal('server-key');
|
|
250
|
+
(0, chai_1.expect)(config_1.config.openaiApiKey).to.equal(undefined);
|
|
251
|
+
const rows = yield scratch.db.webConfig.findMany();
|
|
252
|
+
(0, chai_1.expect)(rows.map((row) => row.name)).to.deep.equal(['aiProvider']);
|
|
253
|
+
(0, chai_1.expect)(JSON.stringify(rows)).not.to.include('page-key');
|
|
254
|
+
}));
|
|
255
|
+
it('are never sent from the started-with base URL either', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
256
|
+
// A key in the address: POST /config refuses one, but the environment
|
|
257
|
+
// can still hold it. GET /config is open to every admin.
|
|
258
|
+
startup({
|
|
259
|
+
aiProvider: 'openai',
|
|
260
|
+
aiBaseUrl: 'https://lab:pw-secret@llm.lab/v1?key=key-secret®ion=eu',
|
|
261
|
+
});
|
|
262
|
+
const shown = (yield read()).body.aiBaseUrl;
|
|
263
|
+
(0, chai_1.expect)(shown).not.to.include('pw-secret');
|
|
264
|
+
(0, chai_1.expect)(shown).not.to.include('key-secret');
|
|
265
|
+
(0, chai_1.expect)(shown).to.include('llm.lab/v1');
|
|
266
|
+
// The server still uses the address as it was given.
|
|
267
|
+
(0, chai_1.expect)(config_1.config.aiBaseUrl).to.equal('https://lab:pw-secret@llm.lab/v1?key=key-secret®ion=eu');
|
|
268
|
+
}));
|
|
269
|
+
it('are never sent, only whether one is set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
270
|
+
startup({ geminiApiKey: 'server-key', aiProvider: 'gemini' });
|
|
271
|
+
const shown = (yield read()).body;
|
|
272
|
+
(0, chai_1.expect)(JSON.stringify(shown)).not.to.include('server-key');
|
|
273
|
+
(0, chai_1.expect)(shown).to.include({ geminiSet: true, openaiSet: false, anthropicSet: false });
|
|
274
|
+
}));
|
|
275
|
+
});
|
|
276
|
+
describe('the model parameters the page used to show', () => {
|
|
277
|
+
it('are not sent: temperature, max tokens and top P were never used by any AI call', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
278
|
+
startup();
|
|
279
|
+
yield save({ aiTemperature: 0.2, aiMaxTokens: 1024, aiTopP: 0.5 });
|
|
280
|
+
const shown = (yield read()).body;
|
|
281
|
+
(0, chai_1.expect)(shown).not.to.have.any.keys('aiTemperature', 'aiMaxTokens', 'aiTopP');
|
|
282
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
|
|
283
|
+
}));
|
|
284
|
+
});
|
|
285
|
+
describe('at boot', () => {
|
|
286
|
+
it('a database that cannot be read leaves the started-with settings in charge, said once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
287
|
+
startup({ aiProvider: 'anthropic' });
|
|
288
|
+
sinon_1.default.stub(web_config_service_1.WebConfigService.prototype, 'getConfig').rejects(new Error('disk I/O error'));
|
|
289
|
+
const warn = sinon_1.default.stub(logger_1.default, 'warn');
|
|
290
|
+
yield restart();
|
|
291
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('anthropic');
|
|
292
|
+
(0, chai_1.expect)(warn.callCount).to.equal(1);
|
|
293
|
+
(0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('disk I/O error');
|
|
294
|
+
}));
|
|
295
|
+
it('a saved value that could not work is skipped, not applied', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
296
|
+
startup({ aiProvider: 'openai' });
|
|
297
|
+
// Written by hand: POST /config refuses it.
|
|
298
|
+
yield scratch.db.webConfig.create({
|
|
299
|
+
data: { id: 'aiProvider', name: 'aiProvider', value: 'gpt' },
|
|
300
|
+
});
|
|
301
|
+
yield restart();
|
|
302
|
+
(0, chai_1.expect)(config_1.config.aiProvider).to.equal('openai');
|
|
303
|
+
}));
|
|
304
|
+
});
|
|
305
|
+
describe('effectiveAiEngine', () => {
|
|
306
|
+
it('takes a saved value over the startup one, field by field', () => {
|
|
307
|
+
(0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({ aiProvider: 'gemini', ollamaModel: 'llama3', aiBaseUrl: 'http://a:1' }, { aiProvider: 'ollama', aiBaseUrl: 'http://b:2' })).to.deep.equal({
|
|
308
|
+
aiProvider: 'ollama',
|
|
309
|
+
aiModel: undefined,
|
|
310
|
+
aiBaseUrl: 'http://b:2',
|
|
311
|
+
geminiModel: undefined,
|
|
312
|
+
openaiModel: undefined,
|
|
313
|
+
anthropicModel: undefined,
|
|
314
|
+
ollamaModel: 'llama3',
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
it('falls back to gemini when neither names a provider', () => {
|
|
318
|
+
(0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({}, {}).aiProvider).to.equal('gemini');
|
|
319
|
+
(0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({}, { aiProvider: '' }).aiProvider).to.equal('gemini');
|
|
320
|
+
});
|
|
321
|
+
it("keeps a started-with provider it doesn't know, rather than pick another for it", () => {
|
|
322
|
+
// AIService then runs with no provider, and says so; a fallback to gemini
|
|
323
|
+
// would send screenshots somewhere the server's owner didn't choose.
|
|
324
|
+
(0, chai_1.expect)((0, aiEngineSettings_1.effectiveAiEngine)({ aiProvider: 'claude' }, {}).aiProvider).to.equal('claude');
|
|
325
|
+
});
|
|
326
|
+
it('names every setting it decides, and no key', () => {
|
|
327
|
+
(0, chai_1.expect)([...aiEngineSettings_1.AI_ENGINE_SETTINGS].sort()).to.deep.equal([
|
|
328
|
+
'aiBaseUrl',
|
|
329
|
+
'aiModel',
|
|
330
|
+
'aiProvider',
|
|
331
|
+
'anthropicModel',
|
|
332
|
+
'geminiModel',
|
|
333
|
+
'ollamaModel',
|
|
334
|
+
'openaiModel',
|
|
335
|
+
].sort());
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const config_1 = require("../../src/config");
|
|
19
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
20
|
+
const LlmHealingProvider_1 = require("../../src/services/healing/LlmHealingProvider");
|
|
21
|
+
const VisualAiHealingProvider_1 = require("../../src/services/healing/VisualAiHealingProvider");
|
|
22
|
+
const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
|
|
23
|
+
/**
|
|
24
|
+
* The AI engine page changes the provider while the server runs (`POST
|
|
25
|
+
* /config` writes `config.aiProvider` and the models). The AI service set its
|
|
26
|
+
* provider up when it was created, and again only inside its own calls, while
|
|
27
|
+
* failure analysis and the LLM and visual healing tiers first asked
|
|
28
|
+
* `isEnabled()`, which read what was set up last. A server started with a
|
|
29
|
+
* provider that had no key never turned those on when a configured one was
|
|
30
|
+
* chosen; one started with a configured provider kept using it after a
|
|
31
|
+
* provider with no key was chosen.
|
|
32
|
+
*/
|
|
33
|
+
describe('AI provider chosen while the server runs', () => {
|
|
34
|
+
const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
|
|
35
|
+
const SCREEN = 'c2NyZWVuc2hvdA==';
|
|
36
|
+
const FAILURE = {
|
|
37
|
+
sessionId: 'sess-switch',
|
|
38
|
+
failureReason: 'An element could not be located on the page',
|
|
39
|
+
commandLogs: [],
|
|
40
|
+
deviceLogs: [],
|
|
41
|
+
};
|
|
42
|
+
afterEach(() => sinon_1.default.restore());
|
|
43
|
+
it('turns on when a provider with a key is chosen after one without', () => {
|
|
44
|
+
ai.use('gemini', { key: false });
|
|
45
|
+
const service = new AIService_1.AIService();
|
|
46
|
+
(0, chai_1.expect)(service.isEnabled()).to.equal(false);
|
|
47
|
+
ai.use('anthropic');
|
|
48
|
+
(0, chai_1.expect)(service.isEnabled()).to.equal(true);
|
|
49
|
+
});
|
|
50
|
+
it('turns off when a provider without a key is chosen, and stops asking the old one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
51
|
+
ai.use('openai');
|
|
52
|
+
ai.answer({ text: 'Root Cause: from OpenAI' });
|
|
53
|
+
const service = new AIService_1.AIService();
|
|
54
|
+
(0, chai_1.expect)(service.isEnabled()).to.equal(true);
|
|
55
|
+
ai.use('anthropic', { key: false });
|
|
56
|
+
(0, chai_1.expect)(service.isEnabled()).to.equal(false);
|
|
57
|
+
(0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal(null);
|
|
58
|
+
(0, chai_1.expect)(ai.calls).to.have.length(0);
|
|
59
|
+
}));
|
|
60
|
+
it('asks the model chosen last', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
ai.use('openai');
|
|
62
|
+
ai.answer({ text: 'Root Cause: x' });
|
|
63
|
+
const service = new AIService_1.AIService();
|
|
64
|
+
yield service.analyzeFailure(FAILURE);
|
|
65
|
+
config_1.config.openaiModel = 'test-openai-chosen-later';
|
|
66
|
+
yield service.analyzeFailure(FAILURE);
|
|
67
|
+
(0, chai_1.expect)(JSON.parse(ai.calls[1].body).model).to.equal('test-openai-chosen-later');
|
|
68
|
+
}));
|
|
69
|
+
it('a provider set only by the plugin options is on at the first heal', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
// AI_SERVICE is made when the plugin loads, before ServerManager applies
|
|
71
|
+
// the plugin's AI options (syncDatabaseAndAIConfig, through updateConfig).
|
|
72
|
+
(0, chai_1.expect)(AIService_1.AI_SERVICE.isEnabled()).to.equal(false);
|
|
73
|
+
(0, config_1.updateConfig)({
|
|
74
|
+
aiProvider: 'ollama',
|
|
75
|
+
aiBaseUrl: fake_ai_provider_1.OLLAMA_URL,
|
|
76
|
+
ollamaModel: `test-ollama-${Math.random().toString(36).slice(2)}`,
|
|
77
|
+
});
|
|
78
|
+
ai.answer({ text: '{"x": 120, "y": 340}' });
|
|
79
|
+
const healed = yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal({
|
|
80
|
+
sessionId: 'sess-switch',
|
|
81
|
+
driver: {},
|
|
82
|
+
strategy: 'id',
|
|
83
|
+
selector: 'login',
|
|
84
|
+
screenshotBase64: SCREEN,
|
|
85
|
+
});
|
|
86
|
+
(0, chai_1.expect)(ai.calls).to.have.length(1);
|
|
87
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.deep.equal({ x: 100, y: 320, width: 40, height: 40 });
|
|
88
|
+
}));
|
|
89
|
+
describe('the healing tiers', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
// The server started with a provider that has no key.
|
|
92
|
+
ai.use('gemini', { key: false });
|
|
93
|
+
(0, chai_1.expect)(AIService_1.AI_SERVICE.isEnabled()).to.equal(false);
|
|
94
|
+
// Then a configured one is chosen on the AI engine page.
|
|
95
|
+
ai.use('anthropic');
|
|
96
|
+
});
|
|
97
|
+
it('the LLM tier asks the provider chosen on the AI engine page', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
ai.answer({
|
|
99
|
+
text: '{"recommendedXpath": "//button[@text=\'Log in\']", "reason": "same text"}',
|
|
100
|
+
});
|
|
101
|
+
const driver = { findElement: sinon_1.default.stub().resolves({ ELEMENT: 'el-1' }) };
|
|
102
|
+
const healed = yield new LlmHealingProvider_1.LlmHealingProvider().heal({
|
|
103
|
+
sessionId: 'sess-switch',
|
|
104
|
+
driver,
|
|
105
|
+
strategy: 'id',
|
|
106
|
+
selector: 'login',
|
|
107
|
+
pageSource: '<hierarchy/>',
|
|
108
|
+
screenshotBase64: SCREEN,
|
|
109
|
+
});
|
|
110
|
+
(0, chai_1.expect)(ai.calls).to.have.length(1);
|
|
111
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('el-1');
|
|
112
|
+
(0, chai_1.expect)(driver.findElement.firstCall.args).to.deep.equal([
|
|
113
|
+
'xpath',
|
|
114
|
+
"//button[@text='Log in']",
|
|
115
|
+
]);
|
|
116
|
+
}));
|
|
117
|
+
it('the visual tier asks the provider chosen on the AI engine page', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
ai.answer({ text: '{"x": 120, "y": 340}' });
|
|
119
|
+
const healed = yield new VisualAiHealingProvider_1.VisualAiHealingProvider().heal({
|
|
120
|
+
sessionId: 'sess-switch',
|
|
121
|
+
driver: {},
|
|
122
|
+
strategy: 'id',
|
|
123
|
+
selector: 'login',
|
|
124
|
+
screenshotBase64: SCREEN,
|
|
125
|
+
});
|
|
126
|
+
(0, chai_1.expect)(ai.calls).to.have.length(1);
|
|
127
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.deep.equal({ x: 100, y: 320, width: 40, height: 40 });
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
15
|
+
const CircuitBreaker_1 = require("../../src/services/CircuitBreaker");
|
|
16
|
+
const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
|
|
17
|
+
/**
|
|
18
|
+
* A rate-limited provider (HTTP 429) is a failed call, whichever provider it
|
|
19
|
+
* is.
|
|
20
|
+
*
|
|
21
|
+
* Gemini's provider answered a 429 with the string
|
|
22
|
+
* 'CONNECTION_OK_RATE_LIMITED' instead of failing, so a failed session's AI
|
|
23
|
+
* analysis was saved as that string and shown on the session page, in the
|
|
24
|
+
* copied report and in bug reports. The circuit breaker counted it as a
|
|
25
|
+
* success, so it never stopped calling a provider that kept refusing. Only the
|
|
26
|
+
* visual assertion and the screen description checked for the string.
|
|
27
|
+
*/
|
|
28
|
+
describe('AI provider rate limits', () => {
|
|
29
|
+
const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
|
|
30
|
+
const FAILURE = {
|
|
31
|
+
sessionId: 'sess-rate-limit',
|
|
32
|
+
failureReason: 'An element could not be located on the page',
|
|
33
|
+
commandLogs: [],
|
|
34
|
+
deviceLogs: [],
|
|
35
|
+
};
|
|
36
|
+
const SCREEN = 'c2NyZWVuc2hvdA==';
|
|
37
|
+
const failureOf = (p) => p.then((value) => {
|
|
38
|
+
throw new Error(`expected a failure, got ${JSON.stringify(value)}`);
|
|
39
|
+
}, (e) => e);
|
|
40
|
+
for (const provider of fake_ai_provider_1.PROVIDERS) {
|
|
41
|
+
describe(provider, () => {
|
|
42
|
+
let service;
|
|
43
|
+
let model;
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
({ model } = ai.use(provider));
|
|
46
|
+
ai.answer({ status: 429 });
|
|
47
|
+
service = new AIService_1.AIService();
|
|
48
|
+
});
|
|
49
|
+
it('gives no failure analysis', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
(0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal(null);
|
|
51
|
+
(0, chai_1.expect)(ai.calls).to.have.length(1);
|
|
52
|
+
}));
|
|
53
|
+
it('counts against the circuit breaker, which stops calling after five', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
for (let i = 0; i < 6; i++)
|
|
55
|
+
yield service.analyzeFailure(FAILURE);
|
|
56
|
+
(0, chai_1.expect)(ai.calls).to.have.length(5);
|
|
57
|
+
const breaker = CircuitBreaker_1.CIRCUIT_BREAKERS.snapshot().find((b) => b.key === `ai:${provider}:${model}`);
|
|
58
|
+
(0, chai_1.expect)(breaker === null || breaker === void 0 ? void 0 : breaker.state).to.equal('open');
|
|
59
|
+
}));
|
|
60
|
+
it('fails a visual assertion, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
const err = yield failureOf(service.assertVisual(SCREEN, 'The cart is empty'));
|
|
62
|
+
(0, chai_1.expect)(err.message).to.match(/rate-limited/);
|
|
63
|
+
(0, chai_1.expect)(err.message).to.match(/not checked/);
|
|
64
|
+
}));
|
|
65
|
+
it('gives no screen description, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
const err = yield failureOf(service.describeScreen(SCREEN));
|
|
67
|
+
(0, chai_1.expect)(err.message).to.match(/rate-limited/);
|
|
68
|
+
}));
|
|
69
|
+
it('fails a locator test, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
const err = yield failureOf(service.visualFind(SCREEN, 'the cart icon', { throwOnError: true }));
|
|
71
|
+
(0, chai_1.expect)(err.message).to.match(/rate-limited/);
|
|
72
|
+
}));
|
|
73
|
+
it('finds nothing for a healing or an ai-icon search', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
(0, chai_1.expect)(yield service.visualFind(SCREEN, 'the cart icon')).to.equal(null);
|
|
75
|
+
(0, chai_1.expect)(yield service.healLocator({ selector: '//x', strategy: 'xpath', xml: '<hierarchy/>' })).to.equal(null);
|
|
76
|
+
}));
|
|
77
|
+
it('says in plain words, once the breaker has opened, that it is waiting', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
for (let i = 0; i < 5; i++)
|
|
79
|
+
yield failureOf(service.describeScreen(SCREEN));
|
|
80
|
+
// The sixth isn't sent. Testers see this (Omni-Scan, Test locator).
|
|
81
|
+
const described = yield failureOf(service.describeScreen(SCREEN));
|
|
82
|
+
(0, chai_1.expect)(described.message).to.match(/has been failing/);
|
|
83
|
+
(0, chai_1.expect)(described.message).to.match(/again in \d+ s/);
|
|
84
|
+
(0, chai_1.expect)(described.message).not.to.match(/circuit|ai:/i);
|
|
85
|
+
const asserted = yield failureOf(service.assertVisual(SCREEN, 'The cart is empty'));
|
|
86
|
+
(0, chai_1.expect)(asserted.message).to.match(/has been failing/);
|
|
87
|
+
(0, chai_1.expect)(asserted.message).to.match(/not checked/);
|
|
88
|
+
(0, chai_1.expect)(asserted.message).not.to.match(/circuit|ai:/i);
|
|
89
|
+
(0, chai_1.expect)(ai.calls).to.have.length(5);
|
|
90
|
+
}));
|
|
91
|
+
it('fails the connection test, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
const result = yield service.testConnection({});
|
|
93
|
+
(0, chai_1.expect)(result.success).to.equal(false);
|
|
94
|
+
(0, chai_1.expect)(result.message).to.match(/rate-limited or out of quota/);
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
it('gemini: an error that only mentions 429 is not a rate limit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
ai.use('gemini');
|
|
100
|
+
ai.answer({
|
|
101
|
+
status: 400,
|
|
102
|
+
message: 'The input token count (1429384) exceeds the maximum number of tokens allowed.',
|
|
103
|
+
});
|
|
104
|
+
const service = new AIService_1.AIService();
|
|
105
|
+
const err = yield failureOf(service.describeScreen(SCREEN));
|
|
106
|
+
(0, chai_1.expect)(err.message).not.to.match(/rate-limited/);
|
|
107
|
+
// A 400 is the request's fault: the breaker doesn't count it, so all six are sent.
|
|
108
|
+
for (let i = 0; i < 5; i++)
|
|
109
|
+
yield service.analyzeFailure(FAILURE);
|
|
110
|
+
(0, chai_1.expect)(ai.calls).to.have.length(6);
|
|
111
|
+
}));
|
|
112
|
+
it('ollama: is asked again as soon as it is back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
ai.use('ollama');
|
|
114
|
+
ai.ollamaUp(false);
|
|
115
|
+
const service = new AIService_1.AIService();
|
|
116
|
+
(0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal(null);
|
|
117
|
+
ai.ollamaUp(true);
|
|
118
|
+
ai.answer({ text: 'Root Cause: the app crashed.' });
|
|
119
|
+
(0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal('Root Cause: the app crashed.');
|
|
120
|
+
}));
|
|
121
|
+
it("still gives a provider's real answer", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
+
ai.use('gemini');
|
|
123
|
+
ai.answer({ text: 'Root Cause: the Login button was covered by a dialog.' });
|
|
124
|
+
(0, chai_1.expect)(yield new AIService_1.AIService().analyzeFailure(FAILURE)).to.equal('Root Cause: the Login button was covered by a dialog.');
|
|
125
|
+
}));
|
|
126
|
+
});
|