@xenon-device-management/xenon 2.14.0 → 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 +7 -4
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.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-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
- package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +5 -5
- 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 +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 +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 +10 -5
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +49 -61
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- 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/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +20 -2
- 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 +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/interceptors/CommandInterceptor.js +243 -181
- 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 +8 -3
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/OrphanSweeper.js +2 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +86 -26
- package/lib/src/services/SocketServer.js +19 -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 +55 -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/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -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/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/mintedLifetime.js +51 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- 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 +57 -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/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/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/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/device-log-book.spec.js +180 -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 +171 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -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/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/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/profile-router.test.js +122 -1
- 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/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 +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.spec.js +347 -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/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/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 +5 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +5 -5
- 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/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -72,6 +72,10 @@ const own_node_id_1 = require("../helpers/own-node-id");
|
|
|
72
72
|
* cannot decode H.264 (WebCodecs exists only in a secure context, so plain
|
|
73
73
|
* http://hub:4723 has none), shows MJPEG, so it says so when it starts the
|
|
74
74
|
* stream (`player: 'mjpeg'`), and the server then runs MJPEG only.
|
|
75
|
+
*
|
|
76
|
+
* Only once nobody plays the H.264 capture, though. A Live devices tile, or a
|
|
77
|
+
* second tab, may still be playing it when one page falls back to MJPEG; the
|
|
78
|
+
* capture then ends when its last viewer leaves, not under them.
|
|
75
79
|
*/
|
|
76
80
|
const UDID = 'DEV-1';
|
|
77
81
|
function buildApp() {
|
|
@@ -98,10 +102,31 @@ describe('POST /control/:udid/stream/start: which capture runs', () => {
|
|
|
98
102
|
let restoreContainer;
|
|
99
103
|
let savedArgs;
|
|
100
104
|
let mjpegStart;
|
|
105
|
+
let h264;
|
|
101
106
|
let h264Start;
|
|
102
107
|
let h264Stop;
|
|
103
|
-
let
|
|
108
|
+
let captureKills;
|
|
109
|
+
let pushPacket;
|
|
104
110
|
let row;
|
|
111
|
+
/** The real H.264 service with its capture (scrcpy) stubbed out. */
|
|
112
|
+
const h264Service = () => {
|
|
113
|
+
const svc = Object.create(AndroidH264StreamService_1.default.prototype);
|
|
114
|
+
svc.sessions = new Map();
|
|
115
|
+
svc.startPromises = new Map();
|
|
116
|
+
svc.scrcpyIncompatible = new Set();
|
|
117
|
+
svc.openCapture = (_udid, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
pushPacket = onPacket;
|
|
119
|
+
onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
|
|
120
|
+
return { kill: () => (captureKills += 1) };
|
|
121
|
+
});
|
|
122
|
+
return svc;
|
|
123
|
+
};
|
|
124
|
+
/** Another page playing the phone's H.264 preview: what it got, and its leave. */
|
|
125
|
+
const h264Viewer = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const got = [];
|
|
127
|
+
const leave = (yield h264.start(UDID)).addClient((p) => got.push(p.type));
|
|
128
|
+
return { got, leave };
|
|
129
|
+
});
|
|
105
130
|
const flag = (androidH264) => {
|
|
106
131
|
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = Object.assign(Object.assign({}, savedArgs), { streaming: { androidH264 } });
|
|
107
132
|
};
|
|
@@ -120,27 +145,27 @@ describe('POST /control/:udid/stream/start: which capture runs', () => {
|
|
|
120
145
|
};
|
|
121
146
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return row; }) });
|
|
122
147
|
sinon_1.default.stub(deviceService, 'blockDevice').resolves();
|
|
123
|
-
|
|
148
|
+
captureKills = 0;
|
|
124
149
|
mjpegStart = sinon_1.default.stub().resolves({ mjpegPort: 9100 });
|
|
125
|
-
|
|
126
|
-
h264Running = true;
|
|
127
|
-
return {};
|
|
128
|
-
}));
|
|
129
|
-
h264Stop = sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
-
h264Running = false;
|
|
131
|
-
}));
|
|
150
|
+
h264 = h264Service();
|
|
132
151
|
typedi_1.Container.set(AndroidStreamService_1.default, {
|
|
133
152
|
startStream: mjpegStart,
|
|
134
153
|
getStreamStatus: () => undefined,
|
|
135
154
|
});
|
|
136
155
|
typedi_1.Container.set(IOSStreamService_1.default, { getStreamStatus: () => undefined });
|
|
137
|
-
typedi_1.Container.set(AndroidH264StreamService_1.default,
|
|
138
|
-
start: h264Start,
|
|
139
|
-
stop: h264Stop,
|
|
140
|
-
getMultiplexer: () => (h264Running ? { clientCount: 0 } : undefined),
|
|
141
|
-
});
|
|
156
|
+
typedi_1.Container.set(AndroidH264StreamService_1.default, h264);
|
|
142
157
|
typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return false; }) });
|
|
158
|
+
// Spied after any setup start, so they count only what the route does.
|
|
159
|
+
h264Start = sinon_1.default.spy(h264, 'start');
|
|
160
|
+
h264Stop = sinon_1.default.spy(h264, 'stop');
|
|
143
161
|
});
|
|
162
|
+
/** An H.264 capture already running for the phone, as a tile's start leaves it. */
|
|
163
|
+
const h264AlreadyRunning = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
164
|
+
h264Start.restore();
|
|
165
|
+
yield h264.start(UDID);
|
|
166
|
+
h264Start = sinon_1.default.spy(h264, 'start');
|
|
167
|
+
});
|
|
168
|
+
const h264Running = () => h264.getMultiplexer(UDID) !== undefined;
|
|
144
169
|
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
170
|
sinon_1.default.restore();
|
|
146
171
|
typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = savedArgs;
|
|
@@ -170,20 +195,39 @@ describe('POST /control/:udid/stream/start: which capture runs', () => {
|
|
|
170
195
|
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
171
196
|
(0, chai_1.expect)(h264Start.called).to.equal(false);
|
|
172
197
|
}));
|
|
173
|
-
it('ends an H.264 capture
|
|
198
|
+
it('ends an H.264 capture nobody is watching, so the two never overlap', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
174
199
|
flag(true);
|
|
175
|
-
|
|
200
|
+
yield h264AlreadyRunning();
|
|
176
201
|
yield start({ player: 'mjpeg' });
|
|
177
202
|
(0, chai_1.expect)(h264Stop.calledOnceWith(UDID)).to.equal(true);
|
|
203
|
+
(0, chai_1.expect)(captureKills).to.equal(1);
|
|
178
204
|
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
179
|
-
(0, chai_1.expect)(h264Running).to.equal(false);
|
|
205
|
+
(0, chai_1.expect)(h264Running()).to.equal(false);
|
|
180
206
|
}));
|
|
181
207
|
it('stops the H.264 capture before it starts the MJPEG one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
208
|
flag(true);
|
|
183
|
-
|
|
209
|
+
yield h264AlreadyRunning();
|
|
184
210
|
yield start({ player: 'mjpeg' });
|
|
185
211
|
(0, chai_1.expect)(h264Stop.calledBefore(mjpegStart)).to.equal(true);
|
|
186
212
|
}));
|
|
213
|
+
it('keeps the H.264 capture for another page still playing it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
flag(true);
|
|
215
|
+
const other = yield h264Viewer();
|
|
216
|
+
const res = yield start({ player: 'mjpeg' });
|
|
217
|
+
(0, chai_1.expect)(res.body.type).to.equal('mjpeg');
|
|
218
|
+
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
219
|
+
(0, chai_1.expect)(captureKills).to.equal(0);
|
|
220
|
+
pushPacket({ type: 'key', data: Buffer.from([1]), ptsMs: 1 });
|
|
221
|
+
(0, chai_1.expect)(other.got).to.deep.equal(['config', 'key']);
|
|
222
|
+
}));
|
|
223
|
+
it('ends the H.264 capture when that last page leaves, without waiting for the idle stop', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
flag(true);
|
|
225
|
+
const other = yield h264Viewer();
|
|
226
|
+
yield start({ player: 'mjpeg' });
|
|
227
|
+
other.leave();
|
|
228
|
+
(0, chai_1.expect)(captureKills).to.equal(1);
|
|
229
|
+
(0, chai_1.expect)(h264Running()).to.equal(false);
|
|
230
|
+
}));
|
|
187
231
|
it('runs MJPEG alone, and stops nothing, when the flag is off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
232
|
flag(false);
|
|
189
233
|
const res = yield start();
|
|
@@ -207,4 +251,14 @@ describe('POST /control/:udid/stream/start: which capture runs', () => {
|
|
|
207
251
|
(0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
|
|
208
252
|
(0, chai_1.expect)(h264Start.called).to.equal(false);
|
|
209
253
|
}));
|
|
254
|
+
it('ends H.264 at once for a phone being recorded, viewers or not, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
// A recording reads the MJPEG capture, and H.264 never runs beside it
|
|
256
|
+
// (ensureMjpegForRecording stopped it when the recording began).
|
|
257
|
+
flag(true);
|
|
258
|
+
yield h264Viewer();
|
|
259
|
+
typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return true; }) });
|
|
260
|
+
yield start();
|
|
261
|
+
(0, chai_1.expect)(captureKills).to.equal(1);
|
|
262
|
+
(0, chai_1.expect)(h264Running()).to.equal(false);
|
|
263
|
+
}));
|
|
210
264
|
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const jose = __importStar(require("jose"));
|
|
51
|
+
const typedi_1 = require("typedi");
|
|
52
|
+
const fs_1 = __importDefault(require("fs"));
|
|
53
|
+
const os_1 = __importDefault(require("os"));
|
|
54
|
+
const path_1 = __importDefault(require("path"));
|
|
55
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
56
|
+
const auth_1 = require("../../src/app/routers/auth");
|
|
57
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
58
|
+
/**
|
|
59
|
+
* A token POST /auth/token mints never carries more than the credential
|
|
60
|
+
* minting it: not more scopes, and not a longer life.
|
|
61
|
+
*
|
|
62
|
+
* - The `sessionToken` beside a xenon-mcp token creates Appium sessions, so
|
|
63
|
+
* it is minted only when the grant includes `appium:use` (which needs the
|
|
64
|
+
* `sessions` scope), and carries the scopes a session is judged by. Any
|
|
65
|
+
* credential used to get one, a read-only key included.
|
|
66
|
+
* - A token minted with a Bearer token, or an API key that expires, ends no
|
|
67
|
+
* later than that credential. A 1-hour token used to mint a fresh 1-hour
|
|
68
|
+
* token, for ever.
|
|
69
|
+
*/
|
|
70
|
+
describe('POST /auth/token mints nothing its creator does not have', () => {
|
|
71
|
+
let dir;
|
|
72
|
+
let restoreContainer;
|
|
73
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService);
|
|
75
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-mint-limits-'));
|
|
76
|
+
const svc = new JwtKeyService_1.JwtKeyService();
|
|
77
|
+
yield svc.init(dir);
|
|
78
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, svc);
|
|
79
|
+
}));
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
restoreContainer();
|
|
82
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
const base = { kind: 'api-key', userId: 'u1', role: 'MEMBER', teamId: null, rateLimit: 300 };
|
|
85
|
+
const auth = (extra) => (Object.assign(Object.assign({}, base), extra));
|
|
86
|
+
describe('the session token', () => {
|
|
87
|
+
it('is not minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const out = yield (0, auth_1.issueToken)(auth({ scopes: 'read' }), { audience: 'xenon-mcp' });
|
|
89
|
+
(0, chai_1.expect)(out.token).to.be.a('string');
|
|
90
|
+
(0, chai_1.expect)(out.sessionToken).to.equal(undefined);
|
|
91
|
+
}));
|
|
92
|
+
it('is not minted when the grant asked for leaves out appium:use', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
|
+
const out = yield (0, auth_1.issueToken)(auth({ scopes: 'devices,sessions,read' }), {
|
|
94
|
+
audience: 'xenon-mcp',
|
|
95
|
+
scopes: ['xenon:devices:read'],
|
|
96
|
+
});
|
|
97
|
+
(0, chai_1.expect)(out.sessionToken).to.equal(undefined);
|
|
98
|
+
}));
|
|
99
|
+
it('carries `sessions`, and `admin` only when its creator has it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
const member = yield (0, auth_1.issueToken)(auth({ scopes: 'devices,sessions,read' }), {
|
|
101
|
+
audience: 'xenon-mcp',
|
|
102
|
+
});
|
|
103
|
+
(0, chai_1.expect)(jose.decodeJwt(String(member.sessionToken)).scopes).to.equal('sessions');
|
|
104
|
+
const dashboardAdmin = yield (0, auth_1.issueToken)(auth({ kind: 'user-session', role: 'ADMIN', scopes: 'admin,devices,sessions,read' }), { audience: 'xenon-mcp' });
|
|
105
|
+
(0, chai_1.expect)(jose.decodeJwt(String(dashboardAdmin.sessionToken)).scopes).to.equal('admin,sessions');
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
// `admin` lets a session take over a lease someone else made. A token asked
|
|
109
|
+
// for with narrower MCP scopes doesn't get it from an admin key's breadth.
|
|
110
|
+
describe("the session token's admin scope follows the grant", () => {
|
|
111
|
+
const adminKey = () => auth({ scopes: 'admin' });
|
|
112
|
+
const scopesOf = (t) => jose.decodeJwt(String(t)).scopes;
|
|
113
|
+
it('is left out when narrower scopes are asked for', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
const out = yield (0, auth_1.issueToken)(adminKey(), { audience: 'xenon-mcp', scopes: ['appium:use'] });
|
|
115
|
+
(0, chai_1.expect)(scopesOf(out.sessionToken)).to.equal('sessions');
|
|
116
|
+
}));
|
|
117
|
+
it('is given for the default grant, and for a full-admin one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
const byDefault = yield (0, auth_1.issueToken)(adminKey(), { audience: 'xenon-mcp' });
|
|
119
|
+
(0, chai_1.expect)(scopesOf(byDefault.sessionToken)).to.equal('admin,sessions');
|
|
120
|
+
const full = yield (0, auth_1.issueToken)(adminKey(), {
|
|
121
|
+
audience: 'xenon-mcp',
|
|
122
|
+
scopes: [
|
|
123
|
+
'appium:use',
|
|
124
|
+
'xenon:analytics:read',
|
|
125
|
+
'xenon:devices:lock',
|
|
126
|
+
'xenon:devices:read',
|
|
127
|
+
'xenon:recordings',
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
(0, chai_1.expect)(scopesOf(full.sessionToken)).to.equal('admin,sessions');
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
133
|
+
describe('its lifetime', () => {
|
|
134
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
135
|
+
for (const audience of ['xenon-rest', 'xenon-mcp']) {
|
|
136
|
+
it(`a ${audience} token ends no later than the credential that minted it`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
const creatorExp = now() + 600;
|
|
138
|
+
const out = yield (0, auth_1.issueToken)(auth({ kind: 'bearer', scopes: 'sessions,read', credentialExpiresAt: creatorExp * 1000 }), { audience });
|
|
139
|
+
const p = jose.decodeJwt(out.token);
|
|
140
|
+
(0, chai_1.expect)(p.exp).to.be.at.most(creatorExp);
|
|
141
|
+
(0, chai_1.expect)(out.expiresIn).to.be.at.most(600);
|
|
142
|
+
(0, chai_1.expect)(out.expiresIn).to.be.at.least(598);
|
|
143
|
+
if (out.sessionToken) {
|
|
144
|
+
(0, chai_1.expect)(jose.decodeJwt(out.sessionToken).exp).to.be.at.most(creatorExp);
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
it('keeps its own lifetime when the credential outlives it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
149
|
+
const out = yield (0, auth_1.issueToken)(auth({ scopes: 'sessions', credentialExpiresAt: Date.now() + 30 * 86400000 }), { audience: 'xenon-rest' });
|
|
150
|
+
(0, chai_1.expect)(out.expiresIn).to.equal(3600);
|
|
151
|
+
}));
|
|
152
|
+
it('is refused when the credential has already expired', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
153
|
+
let caught;
|
|
154
|
+
try {
|
|
155
|
+
yield (0, auth_1.issueToken)(auth({ scopes: 'sessions', credentialExpiresAt: Date.now() - 1000 }), {
|
|
156
|
+
audience: 'xenon-rest',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
caught = err;
|
|
161
|
+
}
|
|
162
|
+
(0, chai_1.expect)(caught === null || caught === void 0 ? void 0 : caught.message).to.match(/expired/);
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const typedi_1 = require("typedi");
|
|
52
|
+
const plugin_1 = require("../../src/plugin");
|
|
53
|
+
const DeviceService = __importStar(require("../../src/data-service/device-service"));
|
|
54
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
55
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
56
|
+
const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
|
|
57
|
+
const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
|
|
58
|
+
const SocketServer_1 = require("../../src/services/SocketServer");
|
|
59
|
+
const MetricsService_1 = require("../../src/services/MetricsService");
|
|
60
|
+
const NotificationService_1 = require("../../src/services/NotificationService");
|
|
61
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
62
|
+
const shutdownReason_1 = require("../../src/services/session/shutdownReason");
|
|
63
|
+
const prisma_1 = require("../../src/prisma");
|
|
64
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
65
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
66
|
+
/**
|
|
67
|
+
* A session Appium ends by itself reaches `XenonPlugin.onUnexpectedShutdown`
|
|
68
|
+
* with a cause. For an idle session that is Appium's new-command timeout.
|
|
69
|
+
* The plugin threw the cause away and recorded "Driver shut down
|
|
70
|
+
* unexpectedly", which no failure pattern matches, so an idle session on this
|
|
71
|
+
* server's own phones was filed UNKNOWN and its "Why it failed" card opened
|
|
72
|
+
* the general runbook rather than the Timeout one.
|
|
73
|
+
*/
|
|
74
|
+
// What @appium/base-driver 10.2 passes when a session sits idle too long.
|
|
75
|
+
const IDLE = "New Command Timeout of 60 seconds expired. Try customizing the timeout using the 'newCommandTimeout' desired capability";
|
|
76
|
+
describe('the reason of a session Appium ends by itself', () => {
|
|
77
|
+
it("is Appium's cause: its new-command timeout", () => {
|
|
78
|
+
(0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(new Error(IDLE))).to.equal(IDLE);
|
|
79
|
+
});
|
|
80
|
+
it("is a driver's own account of giving up, which still reads as one", () => {
|
|
81
|
+
(0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(new Error('The driver was unexpectedly shut down!'))).to.equal('The driver was unexpectedly shut down!');
|
|
82
|
+
});
|
|
83
|
+
it('is the old fixed reason when there is no cause to tell', () => {
|
|
84
|
+
for (const cause of [undefined, null, new Error(''), new Error(' '), {}, 42]) {
|
|
85
|
+
(0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(cause), String(cause)).to.equal(shutdownReason_1.UNEXPECTED_SHUTDOWN_REASON);
|
|
86
|
+
}
|
|
87
|
+
// The umbrella's placeholder when a driver gives none.
|
|
88
|
+
(0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(new Error('Unknown error'))).to.equal(shutdownReason_1.UNEXPECTED_SHUTDOWN_REASON);
|
|
89
|
+
});
|
|
90
|
+
it('is the message alone, never a stack', () => {
|
|
91
|
+
const err = new Error(IDLE);
|
|
92
|
+
(0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(err)).not.to.contain(' at ');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('a session Appium ends for being idle, on a hub that records sessions', function () {
|
|
96
|
+
this.timeout(30000);
|
|
97
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
98
|
+
let restoreRegs;
|
|
99
|
+
let wasHub;
|
|
100
|
+
const plugin = {
|
|
101
|
+
pluginArgs: { enableDashboard: true },
|
|
102
|
+
xenonLog: { withSession: () => ({ info() { } }) },
|
|
103
|
+
};
|
|
104
|
+
const seed = (id) => scratch.db.session.create({
|
|
105
|
+
data: {
|
|
106
|
+
id,
|
|
107
|
+
status: 'running',
|
|
108
|
+
desired_capabilities: '{}',
|
|
109
|
+
session_capabilities: '{}',
|
|
110
|
+
node_id: 'n-1',
|
|
111
|
+
has_live_video: false,
|
|
112
|
+
device_udid: 'R5CT',
|
|
113
|
+
device_platform: 'android',
|
|
114
|
+
device_version: '14',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const shutDown = (id, cause) => plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: id, caps: { udid: 'R5CT' } }, cause);
|
|
118
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService, SocketServer_1.SocketServer, MetricsService_1.MetricsService, NotificationService_1.NotificationService);
|
|
120
|
+
typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession: sinon_1.default.stub().resolves() });
|
|
121
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
|
|
122
|
+
typedi_1.Container.set(SocketServer_1.SocketServer, {
|
|
123
|
+
emitToDashboard: sinon_1.default.stub(),
|
|
124
|
+
emitToDashboardForDevices: sinon_1.default.stub().resolves(),
|
|
125
|
+
hasScopedDashboard: sinon_1.default.stub().returns(false),
|
|
126
|
+
});
|
|
127
|
+
typedi_1.Container.set(MetricsService_1.MetricsService, {
|
|
128
|
+
incrementSessionSuccess: sinon_1.default.stub(),
|
|
129
|
+
incrementSessionFailure: sinon_1.default.stub(),
|
|
130
|
+
});
|
|
131
|
+
typedi_1.Container.set(NotificationService_1.NotificationService, { notifySessionFailed: sinon_1.default.stub().resolves() });
|
|
132
|
+
sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
|
|
133
|
+
sinon_1.default
|
|
134
|
+
.stub(device_store_1.DeviceStoreFactory, 'getStore')
|
|
135
|
+
.returns({ getDevices: () => __awaiter(this, void 0, void 0, function* () { return []; }), findDevices: () => __awaiter(this, void 0, void 0, function* () { return []; }) });
|
|
136
|
+
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
|
|
137
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(false);
|
|
138
|
+
wasHub = plugin_1.XenonPlugin.IS_HUB;
|
|
139
|
+
plugin_1.XenonPlugin.IS_HUB = true;
|
|
140
|
+
yield scratch.db.session.deleteMany({});
|
|
141
|
+
}));
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
plugin_1.XenonPlugin.IS_HUB = wasHub;
|
|
144
|
+
sinon_1.default.restore();
|
|
145
|
+
restoreRegs();
|
|
146
|
+
});
|
|
147
|
+
it("is failed with Appium's reason and filed TIMEOUT", () => __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
yield seed('s-idle');
|
|
149
|
+
yield shutDown('s-idle', new Error(IDLE));
|
|
150
|
+
const row = yield prisma_1.prisma.session.findUnique({ where: { id: 's-idle' } });
|
|
151
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.status).to.equal('failed');
|
|
152
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_reason).to.equal(IDLE);
|
|
153
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_category).to.equal('TIMEOUT');
|
|
154
|
+
}));
|
|
155
|
+
it('keeps the old reason, filed UNKNOWN, when Appium gives no cause', () => __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
yield seed('s-crash');
|
|
157
|
+
yield shutDown('s-crash', undefined);
|
|
158
|
+
const row = yield prisma_1.prisma.session.findUnique({ where: { id: 's-crash' } });
|
|
159
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.status).to.equal('failed');
|
|
160
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_reason).to.equal(shutdownReason_1.UNEXPECTED_SHUTDOWN_REASON);
|
|
161
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_category).to.equal('UNKNOWN');
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
@@ -24,6 +24,7 @@ const UserService_1 = require("../../src/services/UserService");
|
|
|
24
24
|
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
25
25
|
const verifyCredential_1 = require("../../src/middleware/verifyCredential");
|
|
26
26
|
const container_registration_1 = require("../helpers/container-registration");
|
|
27
|
+
const commandCaller_1 = require("../../src/middleware/commandCaller");
|
|
27
28
|
/**
|
|
28
29
|
* The credential checks authMiddleware applies to the header pair and to a
|
|
29
30
|
* Bearer JWT, pulled out so the WebDriver per-command check verifies exactly
|
|
@@ -80,6 +81,38 @@ describe('verifyCredential', () => {
|
|
|
80
81
|
const token = yield keys.sign({ sub: 'u1' }, { audience: 'xenon-rest', ttlSeconds: 60 });
|
|
81
82
|
(0, chai_1.expect)(yield (0, verifyCredential_1.verifyBearerCredential)(token)).to.equal(null);
|
|
82
83
|
}));
|
|
84
|
+
// A token's scopes are fixed when it is minted, for up to a day. An
|
|
85
|
+
// admin demoted to member since keeps no `admin` from it: REST
|
|
86
|
+
// (resolveActor, scopeGuard) and per-command auth's override read these.
|
|
87
|
+
describe("the scopes, by the user's role now", () => {
|
|
88
|
+
const mint = (scopes, audience = 'xenon-rest') => keys.sign({ sub: 'u1', scopes }, { audience, ttlSeconds: 60 });
|
|
89
|
+
const withRole = (role) => typedi_1.Container.get(UserService_1.UserService).findById.resolves(Object.assign(Object.assign({}, active), { role }));
|
|
90
|
+
it('drops admin from the token of a user who is now a member', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
withRole('MEMBER');
|
|
92
|
+
for (const audience of ['xenon-rest', 'xenon-mcp']) {
|
|
93
|
+
const out = yield (0, verifyCredential_1.verifyBearerCredential)(yield mint('admin,devices,sessions,read', audience));
|
|
94
|
+
(0, chai_1.expect)(out === null || out === void 0 ? void 0 : out.payload.scopes, audience).to.equal('devices,sessions,read');
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
it('keeps admin for an ADMIN or SUPER_ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
for (const role of ['ADMIN', 'SUPER_ADMIN']) {
|
|
99
|
+
withRole(role);
|
|
100
|
+
const out = yield (0, verifyCredential_1.verifyBearerCredential)(yield mint('admin,sessions'));
|
|
101
|
+
(0, chai_1.expect)(out === null || out === void 0 ? void 0 : out.payload.scopes, role).to.equal('admin,sessions');
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
it('gives a demoted admin no override in per-command auth', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
withRole('MEMBER');
|
|
106
|
+
const verdict = yield new commandCaller_1.CommandCallerVerifier().verify({
|
|
107
|
+
kind: 'bearer',
|
|
108
|
+
token: yield mint('admin,sessions'),
|
|
109
|
+
});
|
|
110
|
+
(0, chai_1.expect)(verdict).to.deep.equal({
|
|
111
|
+
valid: true,
|
|
112
|
+
caller: { userId: 'u1', overrideAdmin: false },
|
|
113
|
+
});
|
|
114
|
+
}));
|
|
115
|
+
});
|
|
83
116
|
it('throws when the signing key is not available, rather than calling the token invalid', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
117
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, new JwtKeyService_1.JwtKeyService()); // never initialised
|
|
85
118
|
const token = yield keys.sign({ sub: 'u1' }, { audience: 'xenon-rest', ttlSeconds: 60 });
|
|
@@ -94,10 +94,11 @@ describe('xenon: assertVisualState', () => {
|
|
|
94
94
|
(0, chai_1.expect)(err.message).to.match(/429 quota exceeded/);
|
|
95
95
|
}));
|
|
96
96
|
it('fails when the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
-
//
|
|
98
|
-
withProvider('
|
|
97
|
+
// Any provider's 429 (ai-rate-limit.spec.ts).
|
|
98
|
+
withProvider(new AIService_1.AIRateLimitedError('429 Too Many Requests'));
|
|
99
99
|
const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
|
|
100
100
|
(0, chai_1.expect)(err.message).to.match(/rate-limited/i);
|
|
101
|
+
(0, chai_1.expect)(err.message).to.match(/not checked/);
|
|
101
102
|
}));
|
|
102
103
|
it('fails without asking when there is no condition', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
104
|
const call = withProvider('{"result": true, "reason": "ok"}');
|
|
@@ -58,6 +58,25 @@ describe('WebConfigService', () => {
|
|
|
58
58
|
yield service().setConfig({ enableSelfHealing: true });
|
|
59
59
|
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: true });
|
|
60
60
|
}));
|
|
61
|
+
it("keeps the AI engine page's provider, models and base URL, an empty one included", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
|
+
yield service().setConfig({
|
|
63
|
+
aiProvider: 'ollama',
|
|
64
|
+
ollamaModel: 'llava',
|
|
65
|
+
aiBaseUrl: 'http://gpu-box.lab:11434',
|
|
66
|
+
geminiModel: '',
|
|
67
|
+
});
|
|
68
|
+
(0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
|
|
69
|
+
aiProvider: 'ollama',
|
|
70
|
+
ollamaModel: 'llava',
|
|
71
|
+
aiBaseUrl: 'http://gpu-box.lab:11434',
|
|
72
|
+
geminiModel: '',
|
|
73
|
+
});
|
|
74
|
+
}));
|
|
75
|
+
it('never saves an API key, whatever it is sent', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
yield service().setConfig({ aiProvider: 'gemini', geminiApiKey: 'secret' });
|
|
77
|
+
const rows = yield scratch.db.webConfig.findMany();
|
|
78
|
+
(0, chai_1.expect)(rows.map((row) => row.name)).to.deep.equal(['aiProvider']);
|
|
79
|
+
}));
|
|
61
80
|
it('changes a setting saved before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
81
|
yield service().setConfig({ buildCleanupDays: 7, buildCleanupMaxCount: 5 });
|
|
63
82
|
yield service().setConfig({ buildCleanupDays: 14 });
|