@xenon-device-management/xenon 2.13.1 → 2.14.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 +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +19 -46
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +30 -5
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +46 -1
- package/lib/src/data-service/device-store.js +66 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +152 -9
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +143 -37
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/deviceAccessGuard.js +63 -9
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +97 -8
- package/lib/src/services/SessionLifecycleService.js +63 -53
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/lease/activeLeases.js +37 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/busy-precheck.js +24 -3
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-access-lease.spec.js +270 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +114 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/schema-required-args.spec.js +21 -16
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -46
- package/scripts/generate-types-from-schema.js +7 -2
- package/scripts/lib/always-present-plugin-args.js +42 -0
- package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
- package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
- package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
- package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
- package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
- package/lib/public/assets/settings-C1VIluuT.js +0 -6
- package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
|
@@ -179,6 +179,8 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
179
179
|
teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
180
180
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
181
181
|
findDevice: sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () { return device; })),
|
|
182
|
+
// The lease branch records the session's newCommandTimeout on the row.
|
|
183
|
+
updateDevice: sinon_1.default.stub().resolves(),
|
|
182
184
|
});
|
|
183
185
|
sinon_1.default.stub(pendingSessions, 'addNewPendingSession').callsFake((c) => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
186
|
pendingCopy = JSON.parse(JSON.stringify(c));
|
|
@@ -257,6 +259,63 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
257
259
|
(0, chai_1.expect)(findById.calledOnce).to.equal(true);
|
|
258
260
|
(0, chai_1.expect)(teamRows.calledOnce).to.equal(true);
|
|
259
261
|
}));
|
|
262
|
+
describe('the platform allocation lock', () => {
|
|
263
|
+
// Ordinary creates queue on it, first come first served, each with its
|
|
264
|
+
// full wait from the head of the queue. A lease-bound create allocates
|
|
265
|
+
// nothing and used to queue behind them anyway, for up to their wait.
|
|
266
|
+
const plainCaps = () => {
|
|
267
|
+
const caps = sessionCaps(ownerKey);
|
|
268
|
+
delete caps.alwaysMatch['xe:options'].leaseId;
|
|
269
|
+
return caps;
|
|
270
|
+
};
|
|
271
|
+
const tick = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
272
|
+
it("doesn't make a lease-bound create queue behind one waiting for a phone", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
273
|
+
const real = deviceUtils.allocateDeviceForSession;
|
|
274
|
+
let releaseWaiter = () => undefined;
|
|
275
|
+
sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').callsFake(((c, ...rest) => {
|
|
276
|
+
var _a;
|
|
277
|
+
return ((_a = c.alwaysMatch['xe:options']) === null || _a === void 0 ? void 0 : _a.leaseId)
|
|
278
|
+
? real(c, ...rest)
|
|
279
|
+
: new Promise((resolve) => {
|
|
280
|
+
releaseWaiter = resolve;
|
|
281
|
+
});
|
|
282
|
+
}));
|
|
283
|
+
const waiting = create(plainCaps());
|
|
284
|
+
let leased = Promise.resolve();
|
|
285
|
+
try {
|
|
286
|
+
yield tick(50);
|
|
287
|
+
let leasedDone = false;
|
|
288
|
+
leased = create(sessionCaps(ownerKey)).then(() => {
|
|
289
|
+
leasedDone = true;
|
|
290
|
+
});
|
|
291
|
+
yield Promise.race([leased, tick(2000)]);
|
|
292
|
+
(0, chai_1.expect)(leasedDone, 'the lease-bound create waited on the lock').to.equal(true);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
// Free the platform lock even when this fails, and let both creates
|
|
296
|
+
// finish here, or they run on into the next test.
|
|
297
|
+
releaseWaiter(device);
|
|
298
|
+
yield waiting;
|
|
299
|
+
yield leased;
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
it('still queues ordinary creates one at a time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
303
|
+
const waiters = [];
|
|
304
|
+
const allocate = sinon_1.default
|
|
305
|
+
.stub(deviceUtils, 'allocateDeviceForSession')
|
|
306
|
+
.callsFake((() => new Promise((resolve) => waiters.push(resolve))));
|
|
307
|
+
const first = create(plainCaps());
|
|
308
|
+
const second = create(plainCaps());
|
|
309
|
+
yield tick(100);
|
|
310
|
+
(0, chai_1.expect)(allocate.callCount, 'the second create allocated before the first finished').to.equal(1);
|
|
311
|
+
waiters[0](device);
|
|
312
|
+
yield first;
|
|
313
|
+
yield tick(50);
|
|
314
|
+
(0, chai_1.expect)(allocate.callCount).to.equal(2);
|
|
315
|
+
waiters[1](device);
|
|
316
|
+
yield second;
|
|
317
|
+
}));
|
|
318
|
+
});
|
|
260
319
|
it('a session that names a lease reads them once too, for the scope and the lease', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
261
320
|
const findById = sinon_1.default.spy(typedi_1.Container.get(UserService_1.UserService), 'findById');
|
|
262
321
|
yield allowed(sessionCaps(ownerKey));
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = __importDefault(require("chai"));
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const sinon_chai_1 = __importDefault(require("sinon-chai"));
|
|
52
|
+
const ActiveLeases = __importStar(require("../../src/services/lease/activeLeases"));
|
|
53
|
+
const DeviceUtils = __importStar(require("../../src/device-utils"));
|
|
54
|
+
const DeviceService = __importStar(require("../../src/data-service/device-service"));
|
|
55
|
+
const db_1 = require("../../src/data-service/db");
|
|
56
|
+
const device_service_1 = require("../../src/data-service/device-service");
|
|
57
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
58
|
+
const test_container_1 = require("../helpers/test-container");
|
|
59
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
60
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
61
|
+
chai_1.default.use(sinon_chai_1.default);
|
|
62
|
+
const expect = chai_1.default.expect;
|
|
63
|
+
// A phone an SDK lease holds stays its lease's until the lease ends. Its only
|
|
64
|
+
// lock is `busy`, and three things used to undo that: the idle sweeper (which
|
|
65
|
+
// read the lease's creation as the phone's last command), a lease-bound
|
|
66
|
+
// session's release, and allocation, which offered only the first candidate
|
|
67
|
+
// to the lock and so stalled on a leased phone while others were free.
|
|
68
|
+
describe('a phone an SDK lease holds', () => {
|
|
69
|
+
(0, loki_stores_1.useLokiStores)();
|
|
70
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
71
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
72
|
+
const pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, {
|
|
73
|
+
iosDeviceType: 'both',
|
|
74
|
+
androidDeviceType: 'both',
|
|
75
|
+
});
|
|
76
|
+
const phone = (udid, over = {}) => (Object.assign({ udid, name: udid, platform: 'android', sdk: '13', host: 'http://127.0.0.1:4723', busy: false, offline: false, userBlocked: false, state: 'device', deviceType: 'emulator', realDevice: false, systemPort: 57000, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
|
|
77
|
+
const lease = (udid, over = {}) => scratch.db.lease.create({
|
|
78
|
+
data: Object.assign({ tokenHash: 'h', deviceUdid: udid, deviceHost: 'http://127.0.0.1:4723', actorId: 'key_owner', status: 'active', expiresAt: Date.now() + 600000, lastHeartbeatAt: Date.now(), allocatedPorts: '{}', capabilityBag: '{}' }, over),
|
|
79
|
+
});
|
|
80
|
+
const row = (udid) => __awaiter(void 0, void 0, void 0, function* () { return (yield db_1.XenonDatabase.DeviceModel).findOne({ udid }); });
|
|
81
|
+
const setRow = (udid, fields) => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
return (yield db_1.XenonDatabase.DeviceModel)
|
|
83
|
+
.chain()
|
|
84
|
+
.find({ udid })
|
|
85
|
+
.update((d) => Object.assign(d, fields));
|
|
86
|
+
});
|
|
87
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
yield (0, test_container_1.resetTestContainer)();
|
|
89
|
+
yield (0, test_container_1.setupTestContainer)();
|
|
90
|
+
}));
|
|
91
|
+
after(() => (0, test_container_1.resetTestContainer)());
|
|
92
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
|
+
(yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
|
|
94
|
+
yield scratch.db.lease.deleteMany({});
|
|
95
|
+
(0, test_container_1.createTestXenonManager)(Object.assign({}, pluginArgs, { maxSessions: 10, platform: 'android' }));
|
|
96
|
+
sandbox.stub(DeviceUtils, 'setUtilizationTime').resolves();
|
|
97
|
+
}));
|
|
98
|
+
afterEach(() => sandbox.restore());
|
|
99
|
+
describe('the idle sweeper', () => {
|
|
100
|
+
const idle = { busy: true, lastCmdExecutedAt: Date.now() - 600000 };
|
|
101
|
+
it('leaves a leased phone with no session alone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
+
yield (0, device_service_1.addNewDevice)([phone('leased-1')]);
|
|
103
|
+
yield setRow('leased-1', idle);
|
|
104
|
+
yield lease('leased-1');
|
|
105
|
+
const unblock = sandbox.spy(DeviceService, 'unblockDevice');
|
|
106
|
+
yield DeviceUtils.releaseBlockedDevices(20);
|
|
107
|
+
expect(unblock).not.to.have.been.called;
|
|
108
|
+
expect((yield row('leased-1')).busy).to.equal(true);
|
|
109
|
+
}));
|
|
110
|
+
it('frees it once the lease has lapsed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
yield (0, device_service_1.addNewDevice)([phone('lapsed-1')]);
|
|
112
|
+
yield setRow('lapsed-1', idle);
|
|
113
|
+
yield lease('lapsed-1', { expiresAt: Date.now() - 1000 });
|
|
114
|
+
const unblock = sandbox.spy(DeviceService, 'unblockDevice');
|
|
115
|
+
yield DeviceUtils.releaseBlockedDevices(20);
|
|
116
|
+
expect(unblock).to.have.been.calledWith('lapsed-1');
|
|
117
|
+
}));
|
|
118
|
+
it('skips its tick when it cannot tell which phones are leased', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
// Failing open would free every leased phone: their last command is
|
|
120
|
+
// when the lease was taken.
|
|
121
|
+
yield (0, device_service_1.addNewDevice)([phone('plain-3')]);
|
|
122
|
+
yield setRow('plain-3', idle);
|
|
123
|
+
sandbox.stub(ActiveLeases, 'activeLeasesByDevice').rejects(new Error('db down'));
|
|
124
|
+
const unblock = sandbox.spy(DeviceService, 'unblockDevice');
|
|
125
|
+
yield DeviceUtils.releaseBlockedDevices(20);
|
|
126
|
+
expect(unblock).not.to.have.been.called;
|
|
127
|
+
}));
|
|
128
|
+
it('still frees an idle phone nobody leases', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
yield (0, device_service_1.addNewDevice)([phone('plain-1')]);
|
|
130
|
+
yield setRow('plain-1', idle);
|
|
131
|
+
const unblock = sandbox.spy(DeviceService, 'unblockDevice');
|
|
132
|
+
yield DeviceUtils.releaseBlockedDevices(20);
|
|
133
|
+
expect(unblock).to.have.been.calledWith('plain-1');
|
|
134
|
+
}));
|
|
135
|
+
});
|
|
136
|
+
describe("a lease-bound session's release", () => {
|
|
137
|
+
it('hands the phone back to the lease, still busy', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
var _a;
|
|
139
|
+
yield (0, device_service_1.addNewDevice)([phone('leased-2')]);
|
|
140
|
+
yield setRow('leased-2', { busy: true, session_id: 'sess-1' });
|
|
141
|
+
yield lease('leased-2');
|
|
142
|
+
expect(yield DeviceService.releaseSessionDevice('leased-2', 'http://127.0.0.1:4723', 'sess-1')).to.be.true;
|
|
143
|
+
const after = yield row('leased-2');
|
|
144
|
+
expect((_a = after.session_id) !== null && _a !== void 0 ? _a : null).to.equal(null);
|
|
145
|
+
expect(after.busy).to.equal(true);
|
|
146
|
+
}));
|
|
147
|
+
it('undoes the hand-back when the lease ended meanwhile', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
148
|
+
yield (0, device_service_1.addNewDevice)([phone('racing-1')]);
|
|
149
|
+
yield setRow('racing-1', { busy: true, session_id: 'sess-3' });
|
|
150
|
+
// Live at the first check, gone by the second.
|
|
151
|
+
const lookup = sandbox.stub(ActiveLeases, 'activeLeaseOn');
|
|
152
|
+
lookup.onFirstCall().resolves({ id: 'l', actorId: 'k' });
|
|
153
|
+
lookup.resolves(null);
|
|
154
|
+
yield DeviceService.releaseSessionDevice('racing-1', 'http://127.0.0.1:4723', 'sess-3');
|
|
155
|
+
expect(lookup).to.have.been.calledTwice;
|
|
156
|
+
expect((yield row('racing-1')).busy).to.equal(false);
|
|
157
|
+
}));
|
|
158
|
+
it('frees a phone with no lease, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
159
|
+
yield (0, device_service_1.addNewDevice)([phone('plain-2')]);
|
|
160
|
+
yield setRow('plain-2', { busy: true, session_id: 'sess-2' });
|
|
161
|
+
yield DeviceService.releaseSessionDevice('plain-2', 'http://127.0.0.1:4723', 'sess-2');
|
|
162
|
+
expect((yield row('plain-2')).busy).to.equal(false);
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
describe('allocation for a session', () => {
|
|
166
|
+
const caps = (udid) => ({
|
|
167
|
+
alwaysMatch: Object.assign({ platformName: 'android' }, (udid ? { 'appium:udid': udid } : {})),
|
|
168
|
+
firstMatch: [{}],
|
|
169
|
+
});
|
|
170
|
+
it("never locks a reserved phone's twin on another host in its place", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
// Same udid on two hosts: the reserved row is listed first.
|
|
172
|
+
yield (0, device_service_1.addNewDevice)([
|
|
173
|
+
phone('twin', {
|
|
174
|
+
host: 'http://other:4723',
|
|
175
|
+
reservedUntil: Date.now() + 600000,
|
|
176
|
+
reservedBy: 'x',
|
|
177
|
+
}),
|
|
178
|
+
phone('twin'),
|
|
179
|
+
]);
|
|
180
|
+
const got = yield DeviceUtils.allocateDeviceForSession(caps(), 2000, 100, pluginArgs);
|
|
181
|
+
expect(got.host).to.equal('http://127.0.0.1:4723');
|
|
182
|
+
const reserved = (yield db_1.XenonDatabase.DeviceModel).findOne({
|
|
183
|
+
udid: 'twin',
|
|
184
|
+
host: 'http://other:4723',
|
|
185
|
+
});
|
|
186
|
+
expect(reserved.busy).to.equal(false);
|
|
187
|
+
}));
|
|
188
|
+
it('skips a leased phone whose busy flag was cleared and takes a free one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
189
|
+
// Listed first, so the old single-candidate lock tried only it, forever.
|
|
190
|
+
yield (0, device_service_1.addNewDevice)([phone('a-leased'), phone('b-free')]);
|
|
191
|
+
yield lease('a-leased');
|
|
192
|
+
const got = yield DeviceUtils.allocateDeviceForSession(caps(), 2000, 100, pluginArgs);
|
|
193
|
+
expect(got.udid).to.equal('b-free');
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = __importDefault(require("chai"));
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const DeviceUtils = __importStar(require("../../src/device-utils"));
|
|
52
|
+
const db_1 = require("../../src/data-service/db");
|
|
53
|
+
const device_service_1 = require("../../src/data-service/device-service");
|
|
54
|
+
const deviceClaims_1 = require("../../src/data-service/deviceClaims");
|
|
55
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
56
|
+
const test_container_1 = require("../helpers/test-container");
|
|
57
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
58
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
59
|
+
const expect = chai_1.default.expect;
|
|
60
|
+
/**
|
|
61
|
+
* `maxSessions` holds new sessions back once that many Appium sessions run.
|
|
62
|
+
*
|
|
63
|
+
* It compared the number of busy phones with `===`, so once the count was past
|
|
64
|
+
* the limit nothing was held back any more, and it counted every busy phone: a
|
|
65
|
+
* live preview, a recording, or an SDK lease nobody had started a session on
|
|
66
|
+
* used up a slot that no Appium session was using. What it counts now is the
|
|
67
|
+
* phones running a session, or being given one.
|
|
68
|
+
*/
|
|
69
|
+
describe('maxSessions', () => {
|
|
70
|
+
describe('which phones count', () => {
|
|
71
|
+
const base = { busy: true };
|
|
72
|
+
it('counts a session being created (a claim with no session id yet)', () => {
|
|
73
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { claimedAt: Date.now() }))).to.equal(true);
|
|
74
|
+
});
|
|
75
|
+
it('counts a session that is running', () => {
|
|
76
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { claimedAt: 1, claimSessionId: 'sess-1', session_id: 'sess-1' }))).to.equal(true);
|
|
77
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { session_id: 'sess-1' }))).to.equal(true);
|
|
78
|
+
});
|
|
79
|
+
it('does not count a live preview or a recording', () => {
|
|
80
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { session_id: 'manual_u1_phone-1' }))).to.equal(false);
|
|
81
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { session_id: 'manual_phone-1' }))).to.equal(false);
|
|
82
|
+
});
|
|
83
|
+
it('does not count a phone an SDK lease holds that has no session on it', () => {
|
|
84
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign({}, base))).to.equal(false);
|
|
85
|
+
});
|
|
86
|
+
it('counts a session started on a leased phone', () => {
|
|
87
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { claimSessionId: 'sess-2', claimedAt: 1 }))).to.equal(true);
|
|
88
|
+
});
|
|
89
|
+
it('does not count a phone that is not busy', () => {
|
|
90
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)({ busy: false })).to.equal(false);
|
|
91
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)({ busy: false, session_id: 'sess-1' })).to.equal(false);
|
|
92
|
+
});
|
|
93
|
+
it("counts a node's phone the node reports busy, unless the node says it is a preview", () => {
|
|
94
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { nodeBusy: true }))).to.equal(true);
|
|
95
|
+
expect((0, deviceClaims_1.countsTowardMaxSessions)(Object.assign(Object.assign({}, base), { nodeBusy: true, nodeHold: 'manual_u1_phone-1' }))).to.equal(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('the limit itself', () => {
|
|
99
|
+
it('is the setting when it is a positive number', () => {
|
|
100
|
+
expect((0, deviceClaims_1.sessionCap)(8)).to.equal(8);
|
|
101
|
+
expect((0, deviceClaims_1.sessionCap)(1)).to.equal(1);
|
|
102
|
+
});
|
|
103
|
+
it('is no limit when it is unset, zero, negative or not a number', () => {
|
|
104
|
+
for (const value of [undefined, null, 0, -1, NaN, Infinity, '8']) {
|
|
105
|
+
expect((0, deviceClaims_1.sessionCap)(value), String(value)).to.equal(undefined);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('allocation', () => {
|
|
110
|
+
(0, loki_stores_1.useLokiStores)();
|
|
111
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
112
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
113
|
+
const HOST = 'http://127.0.0.1:4723';
|
|
114
|
+
const pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, {
|
|
115
|
+
iosDeviceType: 'both',
|
|
116
|
+
androidDeviceType: 'both',
|
|
117
|
+
});
|
|
118
|
+
const phone = (udid, over = {}) => (Object.assign({ udid, name: udid, platform: 'android', sdk: '13', host: HOST, busy: false, offline: false, userBlocked: false, state: 'device', deviceType: 'emulator', realDevice: false, systemPort: 57000, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
|
|
119
|
+
// New each time: allocation pins the phone it picked into the caps it is given.
|
|
120
|
+
const caps = () => ({
|
|
121
|
+
alwaysMatch: { platformName: 'android' },
|
|
122
|
+
firstMatch: [{}],
|
|
123
|
+
});
|
|
124
|
+
const row = (udid) => __awaiter(void 0, void 0, void 0, function* () { return (yield db_1.XenonDatabase.DeviceModel).findOne({ udid }); });
|
|
125
|
+
/** Asks for a phone, waiting `ms` at most. Resolves to the udid, or null when held back. */
|
|
126
|
+
const ask = (ms = 400) => DeviceUtils.allocateDeviceForSession(caps(), ms, 50, pluginArgs).then((d) => d.udid, () => null);
|
|
127
|
+
const withCap = (maxSessions) => (0, test_container_1.createTestXenonManager)(Object.assign({}, pluginArgs, { maxSessions, platform: 'android' }));
|
|
128
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
yield (0, test_container_1.resetTestContainer)();
|
|
130
|
+
yield (0, test_container_1.setupTestContainer)();
|
|
131
|
+
}));
|
|
132
|
+
after(() => (0, test_container_1.resetTestContainer)());
|
|
133
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
(yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
|
|
135
|
+
yield scratch.db.lease.deleteMany({});
|
|
136
|
+
sandbox.stub(DeviceUtils, 'setUtilizationTime').resolves();
|
|
137
|
+
}));
|
|
138
|
+
afterEach(() => sandbox.restore());
|
|
139
|
+
const running = (n) => Array.from({ length: n }, (_, i) => phone(`running-${i}`, {
|
|
140
|
+
busy: true,
|
|
141
|
+
session_id: `sess-${i}`,
|
|
142
|
+
claimSessionId: `sess-${i}`,
|
|
143
|
+
claimedAt: 1,
|
|
144
|
+
}));
|
|
145
|
+
it('allocates while fewer sessions run than the limit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
withCap(2);
|
|
147
|
+
yield (0, device_service_1.addNewDevice)([...running(1), phone('free-1')]);
|
|
148
|
+
expect(yield ask()).to.equal('free-1');
|
|
149
|
+
}));
|
|
150
|
+
it('holds a session back at the limit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
withCap(2);
|
|
152
|
+
yield (0, device_service_1.addNewDevice)([...running(2), phone('free-1')]);
|
|
153
|
+
expect(yield ask()).to.equal(null);
|
|
154
|
+
expect((yield row('free-1')).busy).to.equal(false);
|
|
155
|
+
}));
|
|
156
|
+
it('still holds it back when more sessions run than the limit, which `===` let through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
|
+
withCap(2);
|
|
158
|
+
yield (0, device_service_1.addNewDevice)([...running(3), phone('free-1')]);
|
|
159
|
+
expect(yield ask()).to.equal(null);
|
|
160
|
+
expect((yield row('free-1')).busy).to.equal(false);
|
|
161
|
+
}));
|
|
162
|
+
it('gives the phone once a session ends', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
163
|
+
withCap(2);
|
|
164
|
+
yield (0, device_service_1.addNewDevice)([...running(2), phone('free-1')]);
|
|
165
|
+
const waiting = ask(2000);
|
|
166
|
+
yield new Promise((r) => setTimeout(r, 150));
|
|
167
|
+
(yield db_1.XenonDatabase.DeviceModel)
|
|
168
|
+
.chain()
|
|
169
|
+
.find({ udid: 'running-0' })
|
|
170
|
+
.update((d) => Object.assign(d, {
|
|
171
|
+
busy: false,
|
|
172
|
+
session_id: null,
|
|
173
|
+
claimSessionId: null,
|
|
174
|
+
claimedAt: null,
|
|
175
|
+
}));
|
|
176
|
+
expect(yield waiting).to.be.oneOf(['free-1', 'running-0']);
|
|
177
|
+
}));
|
|
178
|
+
it('does not count live previews: they are not sessions', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
|
+
withCap(2);
|
|
180
|
+
yield (0, device_service_1.addNewDevice)([
|
|
181
|
+
phone('preview-1', { busy: true, session_id: 'manual_u1_preview-1' }),
|
|
182
|
+
phone('preview-2', { busy: true, session_id: 'manual_u1_preview-2' }),
|
|
183
|
+
phone('free-1'),
|
|
184
|
+
]);
|
|
185
|
+
expect(yield ask()).to.equal('free-1');
|
|
186
|
+
}));
|
|
187
|
+
it('does not count a leased phone no session is running on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
withCap(1);
|
|
189
|
+
yield (0, device_service_1.addNewDevice)([phone('leased-1', { busy: true }), phone('free-1')]);
|
|
190
|
+
yield scratch.db.lease.create({
|
|
191
|
+
data: {
|
|
192
|
+
tokenHash: 'h',
|
|
193
|
+
deviceUdid: 'leased-1',
|
|
194
|
+
deviceHost: HOST,
|
|
195
|
+
actorId: 'key_owner',
|
|
196
|
+
status: 'active',
|
|
197
|
+
expiresAt: Date.now() + 600000,
|
|
198
|
+
lastHeartbeatAt: Date.now(),
|
|
199
|
+
allocatedPorts: '{}',
|
|
200
|
+
capabilityBag: '{}',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
expect(yield ask()).to.equal('free-1');
|
|
204
|
+
}));
|
|
205
|
+
it('counts a session started on a leased phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
withCap(1);
|
|
207
|
+
yield (0, device_service_1.addNewDevice)([
|
|
208
|
+
phone('leased-1', {
|
|
209
|
+
busy: true,
|
|
210
|
+
session_id: 'sess-l',
|
|
211
|
+
claimSessionId: 'sess-l',
|
|
212
|
+
claimedAt: 1,
|
|
213
|
+
}),
|
|
214
|
+
phone('free-1'),
|
|
215
|
+
]);
|
|
216
|
+
expect(yield ask()).to.equal(null);
|
|
217
|
+
}));
|
|
218
|
+
it('counts a session still being created', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
withCap(1);
|
|
220
|
+
yield (0, device_service_1.addNewDevice)([
|
|
221
|
+
phone('pending-1', { busy: true, claimedAt: Date.now() }),
|
|
222
|
+
phone('free-1'),
|
|
223
|
+
]);
|
|
224
|
+
expect(yield ask()).to.equal(null);
|
|
225
|
+
}));
|
|
226
|
+
it('holds back exactly the sessions over the limit when many ask at once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
|
+
withCap(2);
|
|
228
|
+
yield (0, device_service_1.addNewDevice)(Array.from({ length: 6 }, (_, i) => phone(`free-${i}`)));
|
|
229
|
+
const got = yield Promise.all(Array.from({ length: 5 }, () => ask(300)));
|
|
230
|
+
const given = got.filter((udid) => udid !== null);
|
|
231
|
+
expect(given, JSON.stringify(got)).to.have.length(2);
|
|
232
|
+
expect(new Set(given).size).to.equal(2);
|
|
233
|
+
}));
|
|
234
|
+
it('has no limit when the setting is not a positive number', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
235
|
+
withCap(0);
|
|
236
|
+
yield (0, device_service_1.addNewDevice)([...running(3), phone('free-1')]);
|
|
237
|
+
expect(yield ask()).to.equal('free-1');
|
|
238
|
+
}));
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const ocrTextMatch_1 = require("../../src/services/omni-vision/ocrTextMatch");
|
|
5
|
+
describe('ocrTextMatch', () => {
|
|
6
|
+
const word = (text, x0, y0, x1, y1, confidence = 90) => ({ text, x0, y0, x1, y1, confidence });
|
|
7
|
+
it('groups words into lines and runs, in reading order, despite a slightly uneven baseline', () => {
|
|
8
|
+
const runs = (0, ocrTextMatch_1.wordRuns)([
|
|
9
|
+
word('Done', 900, 40, 980, 70),
|
|
10
|
+
word('in', 158, 202, 180, 226),
|
|
11
|
+
word('Cancel', 20, 42, 120, 70),
|
|
12
|
+
word('Sign', 100, 200, 150, 224),
|
|
13
|
+
]);
|
|
14
|
+
(0, chai_1.expect)(runs.map((r) => r.map((w) => w.text))).to.deep.equal([
|
|
15
|
+
['Cancel'],
|
|
16
|
+
['Done'],
|
|
17
|
+
['Sign', 'in'],
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
it('finds every occurrence, once per set of words, with the lowest confidence of the words', () => {
|
|
21
|
+
const words = [
|
|
22
|
+
word('Add', 10, 10, 50, 30, 95),
|
|
23
|
+
word('to', 56, 10, 70, 30, 70),
|
|
24
|
+
word('cart', 76, 10, 120, 30, 88),
|
|
25
|
+
word('Add', 10, 100, 50, 120),
|
|
26
|
+
word('to', 56, 100, 70, 120),
|
|
27
|
+
word('cart', 76, 100, 120, 120),
|
|
28
|
+
];
|
|
29
|
+
const found = (0, ocrTextMatch_1.findText)(words, ' add TO cart ');
|
|
30
|
+
(0, chai_1.expect)(found.map((m) => [m.text, m.y0, m.confidence])).to.deep.equal([
|
|
31
|
+
['Add to cart', 10, 70],
|
|
32
|
+
['Add to cart', 100, 90],
|
|
33
|
+
]);
|
|
34
|
+
(0, chai_1.expect)(found[0]).to.include({ x0: 10, x1: 120, y1: 30 });
|
|
35
|
+
});
|
|
36
|
+
it('matches a one-word text inside a word once, however often it appears there', () => {
|
|
37
|
+
(0, chai_1.expect)((0, ocrTextMatch_1.findText)([word('banana', 0, 0, 60, 20)], 'an').map((m) => m.text)).to.deep.equal([
|
|
38
|
+
'banana',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
it('matches nothing for an empty text', () => {
|
|
42
|
+
(0, chai_1.expect)((0, ocrTextMatch_1.findText)([word('Sign', 0, 0, 40, 20)], ' ')).to.deep.equal([]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
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 OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
19
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
20
|
+
/**
|
|
21
|
+
* `xenon: analyzeScreen` (and device control's Omni-Scan) sent the screenshot's
|
|
22
|
+
* base64 where a file path was expected. The AI step found no such file and
|
|
23
|
+
* asked the provider without an image, under a "why did this test fail"
|
|
24
|
+
* prompt, so `ai_insights` was generic text about no screen at all.
|
|
25
|
+
*
|
|
26
|
+
* Now the screenshot goes to the provider with a prompt about the screen, and
|
|
27
|
+
* when there is no AI answer `ai_insights_error` says why.
|
|
28
|
+
*/
|
|
29
|
+
describe('xenon: analyzeScreen', () => {
|
|
30
|
+
const SCREEN = 'c2NyZWVuc2hvdA==';
|
|
31
|
+
let service;
|
|
32
|
+
const driver = { sessionId: 'session-1', getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return SCREEN; }) };
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
service = new OmniVisionService_1.OmniVisionService();
|
|
35
|
+
sinon_1.default.stub(service, 'performOcr').resolves({
|
|
36
|
+
text: 'Sign in',
|
|
37
|
+
words: [{ text: 'Sign', confidence: 95, bbox: { x0: 10, y0: 10, x1: 50, y1: 30 } }],
|
|
38
|
+
});
|
|
39
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => sinon_1.default.restore());
|
|
42
|
+
it('sends the screenshot to the AI provider and asks about the screen', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value({ analyze: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }) });
|
|
44
|
+
const call = sinon_1.default
|
|
45
|
+
.stub(AIService_1.AI_SERVICE, 'callProvider')
|
|
46
|
+
.resolves('A sign-in screen with an email field.');
|
|
47
|
+
const analysis = yield service.analyzeScreen(driver);
|
|
48
|
+
(0, chai_1.expect)(call.calledOnce).to.equal(true);
|
|
49
|
+
const [prompt, image] = call.firstCall.args;
|
|
50
|
+
(0, chai_1.expect)(image).to.equal(SCREEN);
|
|
51
|
+
(0, chai_1.expect)(prompt).to.match(/screen/i);
|
|
52
|
+
(0, chai_1.expect)(prompt).to.not.match(/root cause|failed/i);
|
|
53
|
+
(0, chai_1.expect)(analysis.ai_insights).to.equal('A sign-in screen with an email field.');
|
|
54
|
+
(0, chai_1.expect)(analysis).to.not.have.property('ai_insights_error');
|
|
55
|
+
(0, chai_1.expect)(analysis.ocr.text).to.equal('Sign in');
|
|
56
|
+
}));
|
|
57
|
+
it('says why there are no AI insights when no provider is configured', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value(null);
|
|
59
|
+
const analysis = yield service.analyzeScreen(driver);
|
|
60
|
+
(0, chai_1.expect)(analysis.ai_insights).to.equal(null);
|
|
61
|
+
(0, chai_1.expect)(analysis.ai_insights_error).to.match(/No AI provider is configured/);
|
|
62
|
+
(0, chai_1.expect)(analysis.ocr.text).to.equal('Sign in');
|
|
63
|
+
}));
|
|
64
|
+
it('says why there are no AI insights when the provider fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value({ analyze: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }) });
|
|
66
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'callProvider').rejects(new Error('429 quota exceeded'));
|
|
67
|
+
const analysis = yield service.analyzeScreen(driver);
|
|
68
|
+
(0, chai_1.expect)(analysis.ai_insights).to.equal(null);
|
|
69
|
+
(0, chai_1.expect)(analysis.ai_insights_error).to.match(/429 quota exceeded/);
|
|
70
|
+
(0, chai_1.expect)(analysis.ocr.text).to.equal('Sign in');
|
|
71
|
+
}));
|
|
72
|
+
});
|