@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.
Files changed (223) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
  40. package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
  41. package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
  42. package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
  43. package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
  44. package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
  45. package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  47. package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  49. package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
  50. package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
  51. package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
  52. package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
  53. package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
  54. package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
  55. package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
  56. package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
  57. package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-B4rA1Xkv.js} +2 -2
  60. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  61. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  62. package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
  63. package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
  64. package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
  65. package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
  66. package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
  67. package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  69. package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
  70. package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
  71. package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
  72. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  73. package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
  74. package/lib/public/index.html +2 -2
  75. package/lib/schema.json +19 -46
  76. package/lib/src/XenonCapabilityManager.js +22 -5
  77. package/lib/src/app/openapi/control.yaml +71 -20
  78. package/lib/src/app/openapi/grid.yaml +23 -5
  79. package/lib/src/app/openapi/platform.yaml +136 -28
  80. package/lib/src/app/routers/control.js +30 -5
  81. package/lib/src/app/routers/dashboard.js +29 -1
  82. package/lib/src/app/routers/grid.js +32 -10
  83. package/lib/src/app/routers/streamStartConflict.js +3 -0
  84. package/lib/src/app/routers/streamType.js +7 -2
  85. package/lib/src/app/routers/webhook.js +8 -4
  86. package/lib/src/config.js +50 -5
  87. package/lib/src/dashboard/commands.js +185 -141
  88. package/lib/src/dashboard/event-manager.js +39 -6
  89. package/lib/src/data-service/appleFamily.js +43 -0
  90. package/lib/src/data-service/db.js +11 -0
  91. package/lib/src/data-service/device-service.js +46 -1
  92. package/lib/src/data-service/device-store.js +66 -3
  93. package/lib/src/data-service/deviceClaims.js +37 -0
  94. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  95. package/lib/src/data-service/deviceSettings.js +79 -0
  96. package/lib/src/data-service/prisma-store.js +152 -9
  97. package/lib/src/data-service/web-config-service.js +43 -92
  98. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  99. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  100. package/lib/src/device-managers/shellCommands.js +96 -0
  101. package/lib/src/device-utils.js +143 -37
  102. package/lib/src/generated/client/edge.js +17 -3
  103. package/lib/src/generated/client/index-browser.js +14 -0
  104. package/lib/src/generated/client/index.d.ts +2054 -379
  105. package/lib/src/generated/client/index.js +17 -3
  106. package/lib/src/generated/client/package.json +1 -1
  107. package/lib/src/generated/client/schema.prisma +35 -7
  108. package/lib/src/generated/client/wasm.js +14 -0
  109. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  110. package/lib/src/interceptors/xenonScripts.js +55 -0
  111. package/lib/src/interfaces/IPluginArgs.js +1 -2
  112. package/lib/src/middleware/deviceAccessGuard.js +63 -9
  113. package/lib/src/plugin.js +15 -1
  114. package/lib/src/prisma.js +1 -1
  115. package/lib/src/scripts/database-check.js +124 -0
  116. package/lib/src/services/AIService.js +99 -0
  117. package/lib/src/services/CleanupService.js +6 -1
  118. package/lib/src/services/InterceptorService.js +70 -13
  119. package/lib/src/services/NetworkConditioningService.js +85 -88
  120. package/lib/src/services/NotificationService.js +36 -29
  121. package/lib/src/services/OrphanSweeper.js +5 -0
  122. package/lib/src/services/PortAllocator.js +5 -0
  123. package/lib/src/services/ServerManager.js +97 -8
  124. package/lib/src/services/SessionLifecycleService.js +63 -53
  125. package/lib/src/services/ShutdownCoordinator.js +33 -0
  126. package/lib/src/services/TeamService.js +20 -3
  127. package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
  128. package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
  129. package/lib/src/services/device-access/leaseHold.js +26 -0
  130. package/lib/src/services/device-access/ticketActorAccess.js +6 -0
  131. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  132. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  133. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  134. package/lib/src/services/lease/activeLeases.js +37 -0
  135. package/lib/src/services/network/AndroidRadios.js +71 -0
  136. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  137. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  138. package/lib/src/services/network/adbForPhone.js +60 -0
  139. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  140. package/lib/src/services/network/xenonProxy.js +107 -0
  141. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  142. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  143. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  144. package/lib/src/services/omni-vision/screenScale.js +68 -0
  145. package/lib/src/services/recording/busy-precheck.js +24 -3
  146. package/lib/src/services/recording/concurrency-gate.js +12 -3
  147. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  148. package/lib/src/services/settings/labSettings.js +149 -0
  149. package/lib/src/services/webhookEvents.js +173 -0
  150. package/lib/test/helpers/fake-adb.js +58 -0
  151. package/lib/test/helpers/fake-device-table.js +63 -24
  152. package/lib/test/helpers/scratch-database.js +1 -0
  153. package/lib/test/unit/TeamService.test.js +3 -0
  154. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  155. package/lib/test/unit/apple-family.spec.js +36 -0
  156. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  157. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  158. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  159. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  160. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  161. package/lib/test/unit/database-check.spec.js +108 -0
  162. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  163. package/lib/test/unit/device-access-lease.spec.js +270 -0
  164. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  165. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  166. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  167. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  168. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  169. package/lib/test/unit/emulator-boot.spec.js +91 -0
  170. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  171. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  172. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  173. package/lib/test/unit/json-logging-option.spec.js +74 -0
  174. package/lib/test/unit/lab-settings.spec.js +218 -0
  175. package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
  176. package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
  177. package/lib/test/unit/lease-held-phone.spec.js +196 -0
  178. package/lib/test/unit/max-sessions.spec.js +240 -0
  179. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  180. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  181. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  182. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  183. package/lib/test/unit/omni-vision.spec.js +1 -1
  184. package/lib/test/unit/partial-config-file.spec.js +114 -0
  185. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  186. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  187. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  188. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  189. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  190. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  191. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  192. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  193. package/lib/test/unit/plugin.spec.js +3 -3
  194. package/lib/test/unit/recording-options.spec.js +181 -0
  195. package/lib/test/unit/schema-required-args.spec.js +21 -16
  196. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  197. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  198. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  199. package/lib/test/unit/shell-commands.spec.js +133 -0
  200. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  201. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  202. package/lib/test/unit/stream-status-type.spec.js +4 -0
  203. package/lib/test/unit/vision-assertion.spec.js +118 -0
  204. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  205. package/lib/test/unit/web-config-service.spec.js +79 -0
  206. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  207. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  208. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  209. package/lib/tsconfig.tsbuildinfo +1 -1
  210. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  211. package/package.json +1 -1
  212. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  213. package/prisma/schema.prisma +35 -7
  214. package/schema.json +19 -46
  215. package/scripts/generate-types-from-schema.js +7 -2
  216. package/scripts/lib/always-present-plugin-args.js +42 -0
  217. package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
  218. package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
  219. package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
  220. package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
  221. package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
  222. package/lib/public/assets/settings-C1VIluuT.js +0 -6
  223. package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
@@ -0,0 +1,91 @@
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 typedi_1 = require("typedi");
19
+ const appium_adb_1 = require("appium-adb");
20
+ const ServerManager_1 = require("../../src/services/ServerManager");
21
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
22
+ /**
23
+ * `emulators` lists Android emulators (AVDs) for the server to boot at
24
+ * startup, with the launch options each one needs. It used to boot them only
25
+ * when `platform` was exactly an Android one, so with the default `both` it
26
+ * booted nothing, and schema.json described it as an allow-list that
27
+ * discovery never applied. Discovery is not filtered by it: a booted emulator
28
+ * is found like any other.
29
+ */
30
+ describe('booting the configured emulators', () => {
31
+ afterEach(() => sinon_1.default.restore());
32
+ function args(over) {
33
+ return Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), over);
34
+ }
35
+ function fakeAdb(launch) {
36
+ const launchAVD = sinon_1.default.stub().callsFake(launch !== null && launch !== void 0 ? launch : (() => __awaiter(this, void 0, void 0, function* () { return undefined; })));
37
+ const createADB = sinon_1.default.stub(appium_adb_1.ADB, 'createADB').resolves({ launchAVD });
38
+ return { launchAVD, createADB };
39
+ }
40
+ const boot = (pluginArgs) => typedi_1.Container.get(ServerManager_1.ServerManager).bootEmulators(pluginArgs);
41
+ const EMULATORS = [
42
+ { avdName: 'Pixel_7', args: ['-no-snapshot-load'] },
43
+ { avdName: 'Pixel_Tablet' },
44
+ ];
45
+ it('boots them when the platform is android', () => __awaiter(void 0, void 0, void 0, function* () {
46
+ const { launchAVD } = fakeAdb();
47
+ yield boot(args({ platform: 'android', emulators: EMULATORS }));
48
+ (0, chai_1.expect)(launchAVD.args.map((a) => a[0])).to.have.members(['Pixel_7', 'Pixel_Tablet']);
49
+ }));
50
+ it('boots them when the platform is both, the default', () => __awaiter(void 0, void 0, void 0, function* () {
51
+ const { launchAVD } = fakeAdb();
52
+ yield boot(args({ platform: 'both', emulators: EMULATORS }));
53
+ (0, chai_1.expect)(launchAVD.args.map((a) => a[0])).to.have.members(['Pixel_7', 'Pixel_Tablet']);
54
+ }));
55
+ it("passes each emulator's own launch options on", () => __awaiter(void 0, void 0, void 0, function* () {
56
+ const { launchAVD } = fakeAdb();
57
+ yield boot(args({ platform: 'both', emulators: EMULATORS }));
58
+ const pixel = launchAVD.args.find((a) => a[0] === 'Pixel_7');
59
+ (0, chai_1.expect)(pixel === null || pixel === void 0 ? void 0 : pixel[1]).to.include({ avdName: 'Pixel_7' });
60
+ (0, chai_1.expect)(pixel === null || pixel === void 0 ? void 0 : pixel[1].args).to.deep.equal(['-no-snapshot-load']);
61
+ }));
62
+ it('boots nothing for an iOS-only server', () => __awaiter(void 0, void 0, void 0, function* () {
63
+ const { launchAVD, createADB } = fakeAdb();
64
+ yield boot(args({ platform: 'ios', emulators: EMULATORS }));
65
+ (0, chai_1.expect)(createADB.called).to.equal(false);
66
+ (0, chai_1.expect)(launchAVD.called).to.equal(false);
67
+ }));
68
+ it('boots nothing when only real Android devices are wanted', () => __awaiter(void 0, void 0, void 0, function* () {
69
+ const { launchAVD } = fakeAdb();
70
+ yield boot(args({ platform: 'both', androidDeviceType: 'real', emulators: EMULATORS }));
71
+ (0, chai_1.expect)(launchAVD.called).to.equal(false);
72
+ }));
73
+ it('boots nothing when none is listed', () => __awaiter(void 0, void 0, void 0, function* () {
74
+ const { launchAVD, createADB } = fakeAdb();
75
+ yield boot(args({ platform: 'both', emulators: [] }));
76
+ (0, chai_1.expect)(createADB.called).to.equal(false);
77
+ (0, chai_1.expect)(launchAVD.called).to.equal(false);
78
+ }));
79
+ it('does not stop the server from starting when one emulator will not boot', () => __awaiter(void 0, void 0, void 0, function* () {
80
+ const { launchAVD } = fakeAdb((avd) => __awaiter(void 0, void 0, void 0, function* () {
81
+ if (avd === 'Pixel_7')
82
+ throw new Error('Emulator is not ready');
83
+ }));
84
+ yield boot(args({ platform: 'both', emulators: EMULATORS }));
85
+ (0, chai_1.expect)(launchAVD.args.map((a) => a[0])).to.have.members(['Pixel_7', 'Pixel_Tablet']);
86
+ }));
87
+ it('does not stop the server from starting when there is no Android SDK', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ sinon_1.default.stub(appium_adb_1.ADB, 'createADB').rejects(new Error('ANDROID_HOME is not set'));
89
+ yield boot(args({ platform: 'both', emulators: EMULATORS }));
90
+ }));
91
+ });
@@ -126,6 +126,8 @@ function fakeDriver(name) {
126
126
  }
127
127
  if (command === 'stopRecordingScreen')
128
128
  return 'node-video.mp4';
129
+ if (command === 'execute')
130
+ return { ranOn: name, script: args[0] };
129
131
  if (command === 'deleteSession') {
130
132
  sessions.delete(args[0]);
131
133
  return null;
@@ -178,6 +180,9 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
178
180
  typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
179
181
  savedStore = device_store_1.DeviceStoreFactory._deviceStore;
180
182
  device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
183
+ // A dashboard-on hub logs each forwarded command against its session.
184
+ yield scratch.db.sessionLog.deleteMany({});
185
+ yield scratch.db.log.deleteMany({});
181
186
  yield scratch.db.session.deleteMany({});
182
187
  yield scratch.db.device.deleteMany({});
183
188
  nodeRequests = [];
@@ -418,6 +423,49 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
418
423
  (0, chai_1.expect)(hub.commands).to.deep.equal([]);
419
424
  }));
420
425
  });
426
+ describe('xenon: execute scripts for a node’s phone', () => {
427
+ // With the hub's dashboard on, its before-hook used to take every
428
+ // `xenon:`/`xe:` script as a dashboard command and answer `{ value: null }`
429
+ // itself, so autowait, Omni-Vision and network-capture scripts for a node's
430
+ // phone never reached the node.
431
+ const execute = (id, script, args = [{}]) => (0, loopbackRequest_1.default)(hubUrl).post(`/wd/hub/session/${id}/execute/sync`).send({ script, args });
432
+ it('forwards Xenon’s scripts other than session details to the node, which answers them', () => __awaiter(this, void 0, void 0, function* () {
433
+ yield boot({ dashboard: true });
434
+ const { id } = yield remoteSession();
435
+ const scripts = [
436
+ 'xenon: setAutowaitProperties',
437
+ 'xe: smartTap',
438
+ 'xenon: assertVisualState',
439
+ 'xenon: addMock',
440
+ 'xenon: notACommand',
441
+ ];
442
+ for (const script of scripts) {
443
+ const res = yield execute(id, script);
444
+ (0, chai_1.expect)(res.status, script).to.equal(200);
445
+ (0, chai_1.expect)(res.body.value, script).to.deep.equal({ ranOn: 'node', script });
446
+ }
447
+ (0, chai_1.expect)(node.commands).to.deep.equal(scripts.map(() => 'execute'));
448
+ (0, chai_1.expect)(hub.commands).to.deep.equal([]);
449
+ }));
450
+ it('answers a session-details command on the hub, from its own record; the node never sees it', () => __awaiter(this, void 0, void 0, function* () {
451
+ yield boot({ dashboard: true });
452
+ const { id } = yield remoteSession();
453
+ const res = yield execute(id, 'xenon: setSessionName', ['Checkout']);
454
+ (0, chai_1.expect)(res.status).to.equal(200);
455
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: { recorded: true } });
456
+ (0, chai_1.expect)(node.commands).to.deep.equal([]);
457
+ (0, chai_1.expect)(hub.commands).to.deep.equal([]);
458
+ const row = yield scratch.db.session.findUnique({ where: { id } });
459
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.name).to.equal('Checkout');
460
+ }));
461
+ it('with the hub’s dashboard off, sends a session-details command on to the node', () => __awaiter(this, void 0, void 0, function* () {
462
+ yield boot({ dashboard: false });
463
+ const { id } = yield remoteSession();
464
+ const res = yield execute(id, 'xenon: setSessionName', ['Checkout']);
465
+ (0, chai_1.expect)(res.body.value).to.deep.equal({ ranOn: 'node', script: 'xenon: setSessionName' });
466
+ (0, chai_1.expect)(node.commands).to.deep.equal(['execute']);
467
+ }));
468
+ });
421
469
  describe('DELETE of a remote session', () => {
422
470
  it('runs the session lifecycle with the forward as its driver step', () => __awaiter(this, void 0, void 0, function* () {
423
471
  yield boot({ nodeAuth: true });
@@ -133,6 +133,11 @@ describe('A hub and a node on one machine keep to their own phones', () => {
133
133
  for (const m of Object.keys(table.delegate)) {
134
134
  sinon_1.default.stub(prisma_1.prisma.device, m).callsFake(table.delegate[m]);
135
135
  }
136
+ // The phones' saved settings (deviceSettings.ts), so none reach a database.
137
+ const settings = (0, fake_device_table_1.fakeDeviceSettingTable)();
138
+ for (const m of Object.keys(settings.delegate)) {
139
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, m).callsFake(settings.delegate[m]);
140
+ }
136
141
  };
137
142
  beforeEach(() => {
138
143
  discover = () => __awaiter(void 0, void 0, void 0, function* () { return []; });
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
13
+ const chai_1 = require("chai");
14
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
15
+ const device_utils_1 = require("../../src/device-utils");
16
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
17
+ const scratch_database_1 = require("../helpers/scratch-database");
18
+ /**
19
+ * `appium:iPhoneOnly` / `appium:iPadOnly` become a device filter in
20
+ * getDeviceFiltersFromCapability, but the Prisma store, the one a running
21
+ * server uses, never applied it: a session asking for an iPad could be given
22
+ * an iPhone. These run the real store against a real (scratch) database, from
23
+ * the capability to the rows that come back.
24
+ *
25
+ * A real iPhone's name is whatever its owner typed ("QA-Phone-3"), so the
26
+ * family is read from what the phone reports (its model, then its form
27
+ * factor), and only from its name for a row written before those existed.
28
+ */
29
+ const HOST = 'http://127.0.0.1:4723';
30
+ const row = (udid, over) => (Object.assign({ udid, host: HOST, platform: 'ios', deviceType: 'real', realDevice: true, busy: false, userBlocked: false }, over));
31
+ const DEVICES = [
32
+ // Simulators: the name is the marketing name, and the form factor comes from it.
33
+ row('sim-iphone', {
34
+ name: 'iPhone 16 Pro',
35
+ deviceType: 'simulator',
36
+ realDevice: false,
37
+ formFactor: 'phone',
38
+ marketingName: 'iPhone 16 Pro',
39
+ }),
40
+ row('sim-ipad', {
41
+ name: 'iPad Pro 13-inch (M4)',
42
+ deviceType: 'simulator',
43
+ realDevice: false,
44
+ formFactor: 'tablet',
45
+ marketingName: 'iPad Pro 13-inch (M4)',
46
+ }),
47
+ row('sim-tv', {
48
+ name: 'Apple TV 4K',
49
+ deviceType: 'simulator',
50
+ realDevice: false,
51
+ formFactor: 'tv',
52
+ marketingName: 'Apple TV 4K',
53
+ }),
54
+ // Real phones, named by their owners.
55
+ row('real-iphone', {
56
+ name: 'QA-Phone-3',
57
+ model: 'iPhone17,3',
58
+ formFactor: 'phone',
59
+ marketingName: 'iPhone 16',
60
+ }),
61
+ row('real-ipad-named-iphone', {
62
+ name: 'iPhone spare',
63
+ model: 'iPad13,4',
64
+ formFactor: 'tablet',
65
+ marketingName: 'iPad Pro 11-inch (3rd generation)',
66
+ }),
67
+ // Rows written before the identity columns: only the name is known.
68
+ row('old-iphone', { name: 'iPhone 13' }),
69
+ row('old-ipad', { name: 'iPad (9th generation)' }),
70
+ row('old-unnamed', { name: 'unknown' }),
71
+ // Android: never an iPhone or an iPad, whatever its form factor.
72
+ row('pixel', {
73
+ name: 'Pixel 7',
74
+ platform: 'android',
75
+ formFactor: 'phone',
76
+ model: 'Pixel 7',
77
+ }),
78
+ row('pixel-tablet', {
79
+ name: 'Pixel Tablet',
80
+ platform: 'android',
81
+ formFactor: 'tablet',
82
+ model: 'Pixel Tablet',
83
+ }),
84
+ ];
85
+ describe('appium:iPhoneOnly and appium:iPadOnly', () => {
86
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
87
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
88
+ yield scratch.db.device.deleteMany({});
89
+ for (const d of DEVICES)
90
+ yield scratch.db.device.create({ data: d });
91
+ }));
92
+ /** The udids a session with these capabilities may be given. */
93
+ function allocatable(caps) {
94
+ return __awaiter(this, void 0, void 0, function* () {
95
+ const filter = (0, device_utils_1.getDeviceFiltersFromCapability)(caps, IPluginArgs_1.DefaultPluginArgs);
96
+ const found = yield new prisma_store_1.PrismaDeviceStore().getDevices(filter);
97
+ return found.map((d) => d.udid).sort();
98
+ });
99
+ }
100
+ it('iPhoneOnly gets iPhones: by model, by form factor, and by name for an old row', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ (0, chai_1.expect)(yield allocatable({ platformName: 'iOS', 'appium:iPhoneOnly': true })).to.deep.equal([
102
+ 'old-iphone',
103
+ 'real-iphone',
104
+ 'sim-iphone',
105
+ ]);
106
+ }));
107
+ it('iPadOnly gets iPads, including one named for an iPhone', () => __awaiter(void 0, void 0, void 0, function* () {
108
+ (0, chai_1.expect)(yield allocatable({ platformName: 'iOS', 'appium:iPadOnly': true })).to.deep.equal([
109
+ 'old-ipad',
110
+ 'real-ipad-named-iphone',
111
+ 'sim-ipad',
112
+ ]);
113
+ }));
114
+ it('gives an iPad when both are asked for, as before', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ (0, chai_1.expect)(yield allocatable({
116
+ platformName: 'iOS',
117
+ 'appium:iPhoneOnly': true,
118
+ 'appium:iPadOnly': true,
119
+ })).to.deep.equal(['old-ipad', 'real-ipad-named-iphone', 'sim-ipad']);
120
+ }));
121
+ it('never gives an Android phone, even when the platform is not named', () => __awaiter(void 0, void 0, void 0, function* () {
122
+ const ids = yield allocatable({ 'appium:iPadOnly': true });
123
+ (0, chai_1.expect)(ids).to.not.include.members(['pixel-tablet']);
124
+ const phones = yield allocatable({ 'appium:iPhoneOnly': true });
125
+ (0, chai_1.expect)(phones).to.not.include.members(['pixel']);
126
+ }));
127
+ it('filters nothing without either capability', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ const all = yield allocatable({ platformName: 'iOS' });
129
+ (0, chai_1.expect)(all).to.include.members(['sim-iphone', 'sim-ipad', 'real-iphone', 'old-unnamed']);
130
+ (0, chai_1.expect)(all).to.not.include.members(['pixel', 'pixel-tablet']);
131
+ }));
132
+ });
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ require("reflect-metadata");
7
+ const chai_1 = require("chai");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const lodash_1 = __importDefault(require("lodash"));
11
+ const plugin_1 = require("../../src/plugin");
12
+ const logger_1 = require("../../src/logger");
13
+ /* eslint-disable @typescript-eslint/no-var-requires -- Appium's internals have no type exports */
14
+ const appiumSchema = require('appium/build/lib/schema/schema');
15
+ /* eslint-enable @typescript-eslint/no-var-requires */
16
+ const xenonSchema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
17
+ /**
18
+ * XENON_JSON_LOGGING turns JSON log lines on. The plugin overwrote it with its
19
+ * own default, `enableJsonLogging: false`, so the variable never worked. The
20
+ * rule now: the option when it is set (either way), else the variable, else
21
+ * off. For that the option has to be absent when nobody set it, so schema.json
22
+ * gives it no default for Appium to fill in.
23
+ */
24
+ describe('JSON logging', () => {
25
+ let savedFlag;
26
+ let savedEnv;
27
+ beforeEach(() => {
28
+ savedFlag = logger_1.XenonLogger.isJsonLogging;
29
+ savedEnv = process.env.XENON_JSON_LOGGING;
30
+ });
31
+ afterEach(() => {
32
+ logger_1.XenonLogger.isJsonLogging = savedFlag;
33
+ if (savedEnv === undefined)
34
+ delete process.env.XENON_JSON_LOGGING;
35
+ else
36
+ process.env.XENON_JSON_LOGGING = savedEnv;
37
+ });
38
+ /** The logger as it stands once the plugin is built the way Appium builds it. */
39
+ function loggingAfterBuilding(cliArgs, env) {
40
+ // The environment is read when the logger loads; this is that read.
41
+ logger_1.XenonLogger.isJsonLogging = env === 'true';
42
+ new plugin_1.XenonPlugin('xenon', cliArgs);
43
+ return logger_1.XenonLogger.isJsonLogging;
44
+ }
45
+ it('is on when only the environment variable asks for it', () => {
46
+ (0, chai_1.expect)(loggingAfterBuilding({}, 'true')).to.equal(true);
47
+ });
48
+ it('is off when nothing asks for it', () => {
49
+ (0, chai_1.expect)(loggingAfterBuilding({})).to.equal(false);
50
+ });
51
+ it('is on when only the option asks for it', () => {
52
+ (0, chai_1.expect)(loggingAfterBuilding({ enableJsonLogging: true })).to.equal(true);
53
+ });
54
+ it('lets an option set to false win over the environment variable', () => {
55
+ (0, chai_1.expect)(loggingAfterBuilding({ enableJsonLogging: false }, 'true')).to.equal(false);
56
+ });
57
+ it('lets an option set to true win over a variable that is not "true"', () => {
58
+ (0, chai_1.expect)(loggingAfterBuilding({ enableJsonLogging: true }, 'false')).to.equal(true);
59
+ });
60
+ describe("Appium's defaults for an option nobody set", () => {
61
+ before(() => {
62
+ appiumSchema.resetSchema();
63
+ appiumSchema.registerSchema('plugin', 'xenon', xenonSchema);
64
+ appiumSchema.finalizeSchema();
65
+ });
66
+ after(() => appiumSchema.resetSchema());
67
+ it('leave enableJsonLogging absent, so the variable can be told from a choice', () => {
68
+ // What Appium's main does: CLI args, then the file, then schema defaults.
69
+ const defaults = appiumSchema.getDefaultsForSchema(false);
70
+ const server = lodash_1.default.defaultsDeep({}, { plugin: { xenon: { maxSessions: 8 } } }, defaults);
71
+ (0, chai_1.expect)(server.plugin.xenon).to.not.have.property('enableJsonLogging');
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ require("reflect-metadata");
15
+ const chai_1 = require("chai");
16
+ const labSettings_1 = require("../../src/services/settings/labSettings");
17
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
18
+ /**
19
+ * The one rule for the settings the dashboard can change: a value saved there,
20
+ * else the plugin option the server started with, else the default.
21
+ */
22
+ describe('lab settings', () => {
23
+ describe('the defaults', () => {
24
+ it('are the ones schema.json declares, which Appium fills in', () => {
25
+ (0, chai_1.expect)((0, labSettings_1.settingsDefaults)()).to.deep.equal({
26
+ enableSelfHealing: true,
27
+ healthCheckIntervalMs: 300000,
28
+ buildCleanupDays: 30,
29
+ buildCleanupMaxCount: 100,
30
+ buildCleanupSchedule: '0 0 * * *',
31
+ deleteBuildAssets: true,
32
+ });
33
+ });
34
+ it('are what the plugin merges under its options', () => {
35
+ for (const [key, value] of Object.entries((0, labSettings_1.settingsDefaults)())) {
36
+ (0, chai_1.expect)(IPluginArgs_1.DefaultPluginArgs[key], key).to.equal(value);
37
+ }
38
+ });
39
+ });
40
+ describe('the settings in effect', () => {
41
+ it('are the defaults when nothing is saved and nothing was set at startup', () => {
42
+ const _a = (0, labSettings_1.effectiveSettings)({}, {}), { healthCheckSchedule } = _a, rest = __rest(_a, ["healthCheckSchedule"]);
43
+ (0, chai_1.expect)(rest).to.deep.equal((0, labSettings_1.settingsDefaults)());
44
+ (0, chai_1.expect)(healthCheckSchedule).to.equal(undefined);
45
+ });
46
+ it('take the startup option over the default', () => {
47
+ const s = (0, labSettings_1.effectiveSettings)({
48
+ buildCleanupDays: 7,
49
+ buildCleanupMaxCount: 20,
50
+ buildCleanupSchedule: '30 1 * * *',
51
+ deleteBuildAssets: false,
52
+ enableSelfHealing: false,
53
+ healthCheckIntervalMs: 600000,
54
+ healthCheckSchedule: '0 * * * *',
55
+ }, {});
56
+ (0, chai_1.expect)(s).to.deep.equal({
57
+ enableSelfHealing: false,
58
+ buildCleanupDays: 7,
59
+ buildCleanupMaxCount: 20,
60
+ buildCleanupSchedule: '30 1 * * *',
61
+ deleteBuildAssets: false,
62
+ healthCheckIntervalMs: 600000,
63
+ healthCheckSchedule: '0 * * * *',
64
+ });
65
+ });
66
+ it('take a saved value over the startup option', () => {
67
+ const s = (0, labSettings_1.effectiveSettings)({
68
+ buildCleanupDays: 7,
69
+ buildCleanupMaxCount: 20,
70
+ buildCleanupSchedule: '30 1 * * *',
71
+ deleteBuildAssets: false,
72
+ healthCheckIntervalMs: 600000,
73
+ }, {
74
+ buildCleanupDays: 14,
75
+ buildCleanupMaxCount: 50,
76
+ buildCleanupSchedule: '0 4 * * 0',
77
+ deleteBuildAssets: true,
78
+ healthCheckIntervalMs: 120000,
79
+ });
80
+ (0, chai_1.expect)(s).to.include({
81
+ buildCleanupDays: 14,
82
+ buildCleanupMaxCount: 50,
83
+ buildCleanupSchedule: '0 4 * * 0',
84
+ deleteBuildAssets: true,
85
+ healthCheckIntervalMs: 120000,
86
+ });
87
+ });
88
+ it('take each setting from its own source', () => {
89
+ const s = (0, labSettings_1.effectiveSettings)({ buildCleanupDays: 7, buildCleanupMaxCount: 20 }, { buildCleanupDays: 14 });
90
+ (0, chai_1.expect)(s.buildCleanupDays).to.equal(14);
91
+ (0, chai_1.expect)(s.buildCleanupMaxCount).to.equal(20);
92
+ });
93
+ it('take a self-healing switch saved off over a startup on, and saved on over a startup off', () => {
94
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: true }, { enableSelfHealing: false })
95
+ .enableSelfHealing).to.equal(false);
96
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: false }, { enableSelfHealing: true })
97
+ .enableSelfHealing).to.equal(true);
98
+ });
99
+ it('take the self-healing switch from the startup option, else on', () => {
100
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: false }, {}).enableSelfHealing).to.equal(false);
101
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({}, {}).enableSelfHealing).to.equal(true);
102
+ });
103
+ it('skip a self-healing switch that is not a boolean, taking the next source', () => {
104
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: false }, { enableSelfHealing: 'true' })
105
+ .enableSelfHealing).to.equal(false);
106
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ enableSelfHealing: 'false' }, {}).enableSelfHealing).to.equal(true);
107
+ });
108
+ it('keep a deleteBuildAssets saved as false over a startup true', () => {
109
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ deleteBuildAssets: true }, { deleteBuildAssets: false })
110
+ .deleteBuildAssets).to.equal(false);
111
+ });
112
+ it('skip a saved value that could not work, taking the next source', () => {
113
+ const s = (0, labSettings_1.effectiveSettings)({ buildCleanupDays: 7, buildCleanupMaxCount: 20, buildCleanupSchedule: '30 1 * * *' }, {
114
+ buildCleanupDays: 0,
115
+ buildCleanupMaxCount: NaN,
116
+ buildCleanupSchedule: 'nightly',
117
+ healthCheckIntervalMs: -5,
118
+ });
119
+ (0, chai_1.expect)(s).to.include({
120
+ buildCleanupDays: 7,
121
+ buildCleanupMaxCount: 20,
122
+ buildCleanupSchedule: '30 1 * * *',
123
+ healthCheckIntervalMs: 300000,
124
+ });
125
+ });
126
+ it('skip a startup value that could not work, taking the default', () => {
127
+ const s = (0, labSettings_1.effectiveSettings)({ buildCleanupDays: 0, buildCleanupSchedule: 'nightly' }, {});
128
+ (0, chai_1.expect)(s.buildCleanupDays).to.equal(30);
129
+ (0, chai_1.expect)(s.buildCleanupSchedule).to.equal('0 0 * * *');
130
+ });
131
+ it('let a health schedule that was cleared in the dashboard stay cleared', () => {
132
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ healthCheckSchedule: '0 * * * *' }, { healthCheckSchedule: '' })
133
+ .healthCheckSchedule).to.equal(undefined);
134
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ healthCheckSchedule: '0 * * * *' }, {}).healthCheckSchedule).to.equal('0 * * * *');
135
+ (0, chai_1.expect)((0, labSettings_1.effectiveSettings)({ healthCheckSchedule: '0 * * * *' }, { healthCheckSchedule: '*/10 * * * *' }).healthCheckSchedule).to.equal('*/10 * * * *');
136
+ });
137
+ });
138
+ describe('whether self-healing runs (what the command interceptor asks)', () => {
139
+ it('is the saved value, else the startup option, else on', () => {
140
+ (0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(true, false)).to.equal(false);
141
+ (0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(false, true)).to.equal(true);
142
+ (0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(false, undefined)).to.equal(false);
143
+ (0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(undefined, undefined)).to.equal(true);
144
+ });
145
+ it('agrees with the settings in effect for every combination', () => {
146
+ for (const startup of [true, false, undefined, 'false', null]) {
147
+ for (const saved of [true, false, undefined]) {
148
+ (0, chai_1.expect)((0, labSettings_1.selfHealingEnabled)(startup, saved), `startup ${String(startup)}, saved ${String(saved)}`).to.equal((0, labSettings_1.effectiveSettings)({ enableSelfHealing: startup }, { enableSelfHealing: saved })
149
+ .enableSelfHealing);
150
+ }
151
+ }
152
+ });
153
+ });
154
+ describe('a cron expression', () => {
155
+ for (const ok of [
156
+ '0 0 * * *',
157
+ '*/15 * * * *',
158
+ '0 2 * * 0',
159
+ '0 */12 * * *',
160
+ '30 1 1 jan *',
161
+ '*/5 * * * * *',
162
+ ]) {
163
+ it(`accepts ${JSON.stringify(ok)}`, () => (0, chai_1.expect)((0, labSettings_1.isValidCron)(ok)).to.equal(true));
164
+ }
165
+ for (const bad of [
166
+ '',
167
+ 'nightly',
168
+ '0 0 * *',
169
+ '60 * * * *',
170
+ '0 0 31 2 *',
171
+ '* * * * * * *',
172
+ '2030-01-01',
173
+ 'Jan 1 2030 10:00:00',
174
+ null,
175
+ 42,
176
+ undefined,
177
+ ]) {
178
+ it(`refuses ${JSON.stringify(bad)}`, () => (0, chai_1.expect)((0, labSettings_1.isValidCron)(bad)).to.equal(false));
179
+ }
180
+ });
181
+ describe('an update', () => {
182
+ it('is accepted when every cleanup field is sound, or none is sent', () => {
183
+ (0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({})).to.equal(null);
184
+ (0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({
185
+ buildCleanupDays: 14,
186
+ buildCleanupMaxCount: 50,
187
+ buildCleanupSchedule: '0 2 * * *',
188
+ deleteBuildAssets: false,
189
+ })).to.equal(null);
190
+ });
191
+ it('is not held to a rule for the settings the cleanup job does not act on', () => {
192
+ (0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({
193
+ healthCheckIntervalMs: 300000,
194
+ aiProvider: 'gemini',
195
+ })).to.equal(null);
196
+ });
197
+ it('accepts the self-healing switch as true or false, and refuses anything else', () => {
198
+ var _a;
199
+ (0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: true })).to.equal(null);
200
+ (0, chai_1.expect)((0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: false })).to.equal(null);
201
+ for (const bad of ['true', 0, null, {}]) {
202
+ (0, chai_1.expect)((_a = (0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: bad })) === null || _a === void 0 ? void 0 : _a.field).to.equal('enableSelfHealing');
203
+ }
204
+ });
205
+ it('names the field a refused value was sent for', () => {
206
+ var _a, _b, _c, _d, _e;
207
+ (0, chai_1.expect)((_a = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupDays: 0 })) === null || _a === void 0 ? void 0 : _a.field).to.equal('buildCleanupDays');
208
+ (0, chai_1.expect)((_b = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupMaxCount: 1.5 })) === null || _b === void 0 ? void 0 : _b.field).to.equal('buildCleanupMaxCount');
209
+ (0, chai_1.expect)((_c = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupSchedule: 'x' })) === null || _c === void 0 ? void 0 : _c.field).to.equal('buildCleanupSchedule');
210
+ (0, chai_1.expect)((_d = (0, labSettings_1.validateSettingsUpdate)({ deleteBuildAssets: 1 })) === null || _d === void 0 ? void 0 : _d.field).to.equal('deleteBuildAssets');
211
+ (0, chai_1.expect)((_e = (0, labSettings_1.validateSettingsUpdate)({ enableSelfHealing: 'yes' })) === null || _e === void 0 ? void 0 : _e.field).to.equal('enableSelfHealing');
212
+ });
213
+ it('treats a field sent as null as sent', () => {
214
+ var _a;
215
+ (0, chai_1.expect)((_a = (0, labSettings_1.validateSettingsUpdate)({ buildCleanupDays: null })) === null || _a === void 0 ? void 0 : _a.field).to.equal('buildCleanupDays');
216
+ });
217
+ });
218
+ });
@@ -41,6 +41,7 @@ describe('allocateDeviceForSession — lease-bound session', () => {
41
41
  let device;
42
42
  let lease;
43
43
  let restore;
44
+ let updateDevice;
44
45
  const allocate = (proof, callerTeamIds) => (0, device_utils_1.allocateDeviceForSession)(leaseCaps(), 1000, 100, IPluginArgs_1.DefaultPluginArgs, callerTeamIds, proof);
45
46
  const refusedWith = (proof, callerTeamIds) => __awaiter(void 0, void 0, void 0, function* () {
46
47
  try {
@@ -71,8 +72,10 @@ describe('allocateDeviceForSession — lease-bound session', () => {
71
72
  const noAuth = { nodePairAuth: () => __awaiter(void 0, void 0, void 0, function* () { return ({ accessKey: '', token: '' }); }) };
72
73
  restore = (0, container_registration_1.saveRegistrations)(LeaseService_1.LeaseService);
73
74
  typedi_1.Container.set(LeaseService_1.LeaseService, new LeaseService_1.LeaseService(db, {}, {}, noAuth));
75
+ updateDevice = sinon_1.default.stub().resolves();
74
76
  sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
75
77
  findDevice: sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () { return device; })),
78
+ updateDevice,
76
79
  });
77
80
  });
78
81
  afterEach(() => {
@@ -94,6 +97,25 @@ describe('allocateDeviceForSession — lease-bound session', () => {
94
97
  (0, chai_1.expect)(got.udid).to.equal('u1');
95
98
  }));
96
99
  });
100
+ describe('the idle sweeper reads the session, not the lease', () => {
101
+ // It used to read the server-wide newCommandTimeoutSec and the lease's
102
+ // creation time, so a leased session was ended a minute after the lease
103
+ // was taken, whatever the session asked for.
104
+ it("records the session's newCommandTimeout and its start", () => __awaiter(void 0, void 0, void 0, function* () {
105
+ const caps = leaseCaps();
106
+ caps.alwaysMatch['appium:newCommandTimeout'] = 120;
107
+ const before = Date.now();
108
+ yield (0, device_utils_1.allocateDeviceForSession)(caps, 1000, 100, IPluginArgs_1.DefaultPluginArgs, undefined, Object.assign(Object.assign({}, NOBODY), { leaseToken: TOKEN }));
109
+ const [udid, host, written] = updateDevice.firstCall.args;
110
+ (0, chai_1.expect)([udid, host]).to.deep.equal(['u1', 'h1']);
111
+ (0, chai_1.expect)(written.newCommandTimeout).to.equal(120);
112
+ (0, chai_1.expect)(written.lastCmdExecutedAt).to.be.at.least(before);
113
+ }));
114
+ it("falls back to the server's newCommandTimeoutSec", () => __awaiter(void 0, void 0, void 0, function* () {
115
+ yield allocate(Object.assign(Object.assign({}, NOBODY), { leaseToken: TOKEN }));
116
+ (0, chai_1.expect)(updateDevice.firstCall.args[2].newCommandTimeout).to.equal(IPluginArgs_1.DefaultPluginArgs.newCommandTimeoutSec);
117
+ }));
118
+ });
97
119
  describe("someone else's lease id is refused, as if it were not active", () => {
98
120
  it('with no credentials and no token', () => __awaiter(void 0, void 0, void 0, function* () {
99
121
  (0, chai_1.expect)(yield refusal(NOBODY)).to.equal(REFUSED);