@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,182 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const plugin_1 = require("../../../src/plugin");
53
+ const NodeDevices_1 = __importDefault(require("../../../src/device-managers/NodeDevices"));
54
+ const DeviceService = __importStar(require("../../../src/data-service/device-service"));
55
+ const ActiveLeases = __importStar(require("../../../src/services/lease/activeLeases"));
56
+ const device_utils_1 = require("../../../src/device-utils");
57
+ const SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
58
+ const SessionMetricsService_1 = require("../../../src/services/metrics/SessionMetricsService");
59
+ const OrphanSweeper_1 = require("../../../src/services/OrphanSweeper");
60
+ const ShutdownCoordinator_1 = require("../../../src/services/ShutdownCoordinator");
61
+ const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
62
+ const event_manager_1 = require("../../../src/dashboard/event-manager");
63
+ const SessionManager_1 = require("../../../src/sessions/SessionManager");
64
+ const prisma_1 = require("../../../src/prisma");
65
+ const container_registration_1 = require("../../helpers/container-registration");
66
+ const scratch_database_1 = require("../../helpers/scratch-database");
67
+ /**
68
+ * A network profile or the interceptor changes the whole phone. Its network
69
+ * was put back only when the test deleted a session Xenon kept in memory.
70
+ * Every other ending (Appium's new-command timeout, Xenon's idle release, a
71
+ * shutdown, a stale heartbeat) left an Offline phone offline and an
72
+ * intercepted phone pointing at a proxy that no longer exists.
73
+ *
74
+ * Each ending now asks PhoneNetworkRestore, keyed by the session id, and
75
+ * before the phone is released: a phone handed to the next session while its
76
+ * network is still being put back could have that session's own profile
77
+ * undone.
78
+ */
79
+ describe('a session’s phone network is put back however the session ends', function () {
80
+ this.timeout(30000);
81
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
82
+ let restoreRegs;
83
+ let order;
84
+ let restoreSession;
85
+ let restoreAll;
86
+ beforeEach(() => {
87
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
88
+ order = [];
89
+ restoreSession = sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () {
90
+ order.push(`restore ${id}`);
91
+ }));
92
+ restoreAll = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
93
+ order.push('restore all');
94
+ }));
95
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession, restoreAll });
96
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
97
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').callsFake((id) => __awaiter(this, void 0, void 0, function* () {
98
+ order.push(`release ${id}`);
99
+ }));
100
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
101
+ });
102
+ afterEach(() => {
103
+ sinon_1.default.restore();
104
+ restoreRegs();
105
+ });
106
+ it('when the test deletes a session Xenon keeps no record of', () => __awaiter(this, void 0, void 0, function* () {
107
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('s-untracked')).to.equal(undefined);
108
+ const next = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
109
+ order.push('driver quit');
110
+ }));
111
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(next, 's-untracked');
112
+ (0, chai_1.expect)(restoreSession.callCount).to.equal(1);
113
+ (0, chai_1.expect)(order.indexOf('restore s-untracked')).to.be.lessThan(order.indexOf('release s-untracked'));
114
+ }));
115
+ it("when Appium ends the session itself (new-command timeout), on this server's own phone", () => __awaiter(this, void 0, void 0, function* () {
116
+ const plugin = { pluginArgs: {}, xenonLog: { withSession: () => ({ info() { } }) } };
117
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-timeout', caps: { udid: 'R5CT' } }, new Error('New command timeout'));
118
+ (0, chai_1.expect)(restoreSession.callCount).to.equal(1);
119
+ (0, chai_1.expect)(restoreSession.firstCall.args[0]).to.equal('s-timeout');
120
+ (0, chai_1.expect)(order).to.deep.equal(['restore s-timeout', 'release s-timeout']);
121
+ }));
122
+ it('when Appium ends the session on a node', () => __awaiter(this, void 0, void 0, function* () {
123
+ const unblock = sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').callsFake(() => __awaiter(this, void 0, void 0, function* () {
124
+ order.push('hub unblock');
125
+ return undefined;
126
+ }));
127
+ const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
128
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-node', caps: {} }, null);
129
+ (0, chai_1.expect)(unblock.calledOnce).to.equal(true);
130
+ (0, chai_1.expect)(order).to.deep.equal(['restore s-node', 'hub unblock']);
131
+ }));
132
+ it("when Xenon's idle check releases the phone", () => __awaiter(this, void 0, void 0, function* () {
133
+ const idle = {
134
+ udid: 'R5CT',
135
+ host: 'http://127.0.0.1:4723',
136
+ busy: true,
137
+ userBlocked: false,
138
+ session_id: 's-idle',
139
+ claimSessionId: 's-idle',
140
+ lastCmdExecutedAt: Date.now() - 10 * 60 * 1000,
141
+ };
142
+ sinon_1.default.stub(DeviceService, 'getAllDevices').resolves([idle]);
143
+ sinon_1.default.stub(ActiveLeases, 'activeLeasesByDevice').resolves(new Map());
144
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevice').callsFake((_u, _h, id) => __awaiter(this, void 0, void 0, function* () {
145
+ order.push(`release ${id}`);
146
+ return true;
147
+ }));
148
+ yield (0, device_utils_1.releaseBlockedDevices)(60);
149
+ (0, chai_1.expect)(restoreSession.callCount).to.equal(1);
150
+ (0, chai_1.expect)(order).to.deep.equal(['restore s-idle', 'release s-idle']);
151
+ }));
152
+ it('when a graceful shutdown drains a session', () => __awaiter(this, void 0, void 0, function* () {
153
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
154
+ (0, chai_1.expect)(order.slice(0, 2)).to.deep.equal(['restore s-down', 'release s-down']);
155
+ }));
156
+ it('when a graceful shutdown ends, for sessions Xenon keeps no record of', () => __awaiter(this, void 0, void 0, function* () {
157
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getAllSessions').returns([]);
158
+ yield new ShutdownCoordinator_1.ShutdownCoordinator().drain(1000, 500);
159
+ (0, chai_1.expect)(restoreAll.calledOnce).to.equal(true);
160
+ }));
161
+ it("when a session's heartbeat goes stale", () => __awaiter(this, void 0, void 0, function* () {
162
+ var _a;
163
+ yield scratch.db.session.deleteMany({});
164
+ yield scratch.db.session.create({
165
+ data: {
166
+ id: 's-stale',
167
+ status: 'running',
168
+ desired_capabilities: '{}',
169
+ session_capabilities: '{}',
170
+ node_id: 'n',
171
+ has_live_video: false,
172
+ device_udid: 'R5CT',
173
+ device_platform: 'android',
174
+ device_version: '14',
175
+ last_heartbeat_at: new Date(Date.now() - 60 * 60 * 1000),
176
+ },
177
+ });
178
+ yield new OrphanSweeper_1.OrphanSweeper().sweep({ heartbeatIntervalMs: 1000 });
179
+ (0, chai_1.expect)(order.slice(0, 2)).to.deep.equal(['restore s-stale', 'release s-stale']);
180
+ (0, chai_1.expect)((_a = (yield prisma_1.prisma.session.findUnique({ where: { id: 's-stale' } }))) === null || _a === void 0 ? void 0 : _a.status).to.equal('failed');
181
+ }));
182
+ });
@@ -0,0 +1,110 @@
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 SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
20
+ const NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
21
+ const InterceptorService_1 = require("../../../src/services/InterceptorService");
22
+ const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
23
+ const PluginContext_1 = require("../../../src/PluginContext");
24
+ const IPluginArgs_1 = require("../../../src/interfaces/IPluginArgs");
25
+ const XenonCapabilityManager_1 = require("../../../src/XenonCapabilityManager");
26
+ const SessionManager_1 = require("../../../src/sessions/SessionManager");
27
+ const SessionType_1 = __importDefault(require("../../../src/enums/SessionType"));
28
+ const container_registration_1 = require("../../helpers/container-registration");
29
+ const scratch_database_1 = require("../../helpers/scratch-database");
30
+ const android = { udid: 'R5CT', platform: 'android', deviceType: 'real', host: 'h' };
31
+ const sessionOf = (type) => ({
32
+ getId: () => 's1',
33
+ getType: () => type,
34
+ getCapabilities: () => ({}),
35
+ apiKeyId: null,
36
+ userId: null,
37
+ startVideoRecording: sinon_1.default.stub().resolves(),
38
+ });
39
+ const capsWith = (extra) => (Object.assign({ [XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING]: false, [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS]: [] }, extra));
40
+ /**
41
+ * Where a session's network changes are made. Only the server that drives the
42
+ * phone makes them: a hub used to run its own adb against a node's phone (on
43
+ * one Mac, the very same phone), and start a second proxy for it.
44
+ */
45
+ describe('a session’s network changes at its start', function () {
46
+ this.timeout(30000);
47
+ // The row a hub writes for a session another server runs (recordRoutedSession).
48
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
49
+ let restoreRegs;
50
+ let order;
51
+ let applyProfile;
52
+ let start;
53
+ let restoreLeftovers;
54
+ let context;
55
+ beforeEach(() => {
56
+ restoreRegs = (0, container_registration_1.saveRegistrations)(NetworkConditioningService_1.NetworkConditioningService, InterceptorService_1.InterceptorService, PhoneNetworkRestore_1.PhoneNetworkRestore, PluginContext_1.PluginContext);
57
+ order = [];
58
+ applyProfile = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('profile'); }));
59
+ start = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('interceptor'); }));
60
+ restoreLeftovers = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
61
+ order.push('leftovers');
62
+ return 0;
63
+ }));
64
+ typedi_1.Container.set(NetworkConditioningService_1.NetworkConditioningService, { applyProfile });
65
+ typedi_1.Container.set(InterceptorService_1.InterceptorService, { start });
66
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreLeftovers });
67
+ context = new PluginContext_1.PluginContext();
68
+ context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
69
+ typedi_1.Container.set(PluginContext_1.PluginContext, context);
70
+ });
71
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
72
+ sinon_1.default.restore();
73
+ SessionManager_1.SESSION_MANAGER.removeSession('s1');
74
+ restoreRegs();
75
+ yield scratch.db.session.deleteMany({});
76
+ }));
77
+ const apply = (type, caps) => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).applyPostSessionLogic(sessionOf(type), caps, android);
78
+ it('puts back what an earlier session left on the phone, then applies its own', () => __awaiter(this, void 0, void 0, function* () {
79
+ yield apply(SessionType_1.default.LOCAL, capsWith({
80
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
81
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
82
+ }));
83
+ (0, chai_1.expect)(restoreLeftovers.calledOnceWith({ udid: 'R5CT' })).to.equal(true);
84
+ (0, chai_1.expect)(order).to.deep.equal(['leftovers', 'profile', 'interceptor']);
85
+ }));
86
+ it("doesn't touch a phone another server drives", () => __awaiter(this, void 0, void 0, function* () {
87
+ for (const type of [SessionType_1.default.REMOTE, SessionType_1.default.CLOUD]) {
88
+ yield apply(type, capsWith({
89
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
90
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
91
+ }));
92
+ SessionManager_1.SESSION_MANAGER.removeSession('s1');
93
+ }
94
+ (0, chai_1.expect)(order).to.deep.equal([]);
95
+ }));
96
+ it("starts the interceptor by the server's option when the session doesn't say", () => __awaiter(this, void 0, void 0, function* () {
97
+ context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true, bufferSize: 9 } });
98
+ yield apply(SessionType_1.default.LOCAL, capsWith({}));
99
+ (0, chai_1.expect)(start.calledOnce).to.equal(true);
100
+ const [sessionId, device, opts] = start.firstCall.args;
101
+ (0, chai_1.expect)(sessionId).to.equal('s1');
102
+ (0, chai_1.expect)(device).to.equal(android);
103
+ (0, chai_1.expect)(opts).to.include({ enabled: true, bufferSize: 9, captureBodies: true });
104
+ }));
105
+ it("leaves the interceptor off when the session turns it off over the server's option", () => __awaiter(this, void 0, void 0, function* () {
106
+ context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true } });
107
+ yield apply(SessionType_1.default.LOCAL, capsWith({ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: false }));
108
+ (0, chai_1.expect)(start.called).to.equal(false);
109
+ }));
110
+ });
@@ -0,0 +1,69 @@
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 net_1 = __importDefault(require("net"));
18
+ const xenonProxy_1 = require("../../../src/services/network/xenonProxy");
19
+ describe('a phone proxy setting, read', () => {
20
+ it('parses host:port', () => {
21
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('127.0.0.1:11100')).to.deep.equal({ host: '127.0.0.1', port: 11100 });
22
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(' 10.0.2.2:11105 ')).to.deep.equal({ host: '10.0.2.2', port: 11105 });
23
+ });
24
+ it('reads no proxy from what Android stores for none', () => {
25
+ for (const none of [null, undefined, '', 'null', ':0', ' ']) {
26
+ (0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)(none), String(none)).to.equal(true);
27
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(none)).to.equal(null);
28
+ }
29
+ (0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)('proxy.corp:3128')).to.equal(false);
30
+ });
31
+ it('refuses a value that is not host:port', () => {
32
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('proxy.corp')).to.equal(null);
33
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:b')).to.equal(null);
34
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:0')).to.equal(null);
35
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:70000')).to.equal(null);
36
+ });
37
+ it('knows the addresses a phone uses to reach this machine', () => {
38
+ const own = ['192.168.1.20'];
39
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('127.0.0.1', own)).to.equal(true);
40
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('localhost', own)).to.equal(true);
41
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('10.0.2.2', own)).to.equal(true);
42
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.20', own)).to.equal(true);
43
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.21', own)).to.equal(false);
44
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('proxy.corp', own)).to.equal(false);
45
+ });
46
+ });
47
+ describe('isPortListening', function () {
48
+ let server;
49
+ let livePort;
50
+ let deadPort;
51
+ before(() => __awaiter(this, void 0, void 0, function* () {
52
+ server = net_1.default.createServer((s) => s.destroy());
53
+ yield new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
54
+ livePort = server.address().port;
55
+ const closed = net_1.default.createServer();
56
+ yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
57
+ deadPort = closed.address().port;
58
+ yield new Promise((r) => closed.close(() => r()));
59
+ }));
60
+ after(() => __awaiter(this, void 0, void 0, function* () {
61
+ yield new Promise((r) => server.close(() => r()));
62
+ }));
63
+ it('says a port with a listener answers', () => __awaiter(this, void 0, void 0, function* () {
64
+ (0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(livePort)).to.equal(true);
65
+ }));
66
+ it('says a closed port does not', () => __awaiter(this, void 0, void 0, function* () {
67
+ (0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(deadPort)).to.equal(false);
68
+ }));
69
+ });
@@ -81,7 +81,7 @@ describe('Device filter tests', () => {
81
81
  (0, chai_1.expect)(filter).to.deep.equal({
82
82
  platform: 'ios',
83
83
  platformVersion: '14.0',
84
- name: 'iPhone',
84
+ appleFamily: 'iphone',
85
85
  deviceType: 'real',
86
86
  udid: ['21112-1111-1111-111'],
87
87
  minSDK: undefined,
@@ -113,7 +113,7 @@ describe('Device filter tests', () => {
113
113
  (0, chai_1.expect)(filter).to.deep.equal({
114
114
  platform: 'ios',
115
115
  platformVersion: '14.0',
116
- name: 'iPhone',
116
+ appleFamily: 'iphone',
117
117
  filterByHost: undefined,
118
118
  deviceType: 'simulator',
119
119
  udid: [],
@@ -140,7 +140,7 @@ describe('Device filter tests', () => {
140
140
  platform: 'ios',
141
141
  filterByHost: undefined,
142
142
  platformVersion: undefined,
143
- name: 'iPhone',
143
+ appleFamily: 'iphone',
144
144
  deviceType: 'simulator',
145
145
  udid: [],
146
146
  minSDK: '10.2.0',
@@ -0,0 +1,181 @@
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 plugin_1 = require("../../src/plugin");
20
+ const ServerManager_1 = require("../../src/services/ServerManager");
21
+ const PluginContext_1 = require("../../src/PluginContext");
22
+ const concurrency_gate_1 = require("../../src/services/recording/concurrency-gate");
23
+ const config_1 = require("../../src/config");
24
+ const logger_1 = __importDefault(require("../../src/logger"));
25
+ /**
26
+ * `maxConcurrentRecordings` and `recordingsAssetsPath` are declared in
27
+ * schema.json, so Appium accepts them in a config file, but nothing read them:
28
+ * only the XENON_MAX_CONCURRENT_RECORDINGS and XENON_RECORDINGS_ASSETS_PATH
29
+ * environment variables reached src/config.ts. The option now wins, the
30
+ * variable is the fallback, and both are applied at boot before anything that
31
+ * reads them (the artifact store, the concurrency gate).
32
+ */
33
+ describe('the recording options', () => {
34
+ let saved;
35
+ let env;
36
+ let statics;
37
+ beforeEach(() => {
38
+ saved = { max: config_1.config.maxConcurrentRecordings, path: config_1.config.recordingsAssetsPath };
39
+ env = {
40
+ max: process.env.XENON_MAX_CONCURRENT_RECORDINGS,
41
+ path: process.env.XENON_RECORDINGS_ASSETS_PATH,
42
+ };
43
+ delete process.env.XENON_MAX_CONCURRENT_RECORDINGS;
44
+ delete process.env.XENON_RECORDINGS_ASSETS_PATH;
45
+ statics = {
46
+ nodeId: plugin_1.XenonPlugin.NODE_ID,
47
+ port: plugin_1.XenonPlugin.port,
48
+ basePath: plugin_1.XenonPlugin.nodeBasePath,
49
+ };
50
+ });
51
+ afterEach(() => {
52
+ sinon_1.default.restore();
53
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: saved.max, recordingsAssetsPath: saved.path });
54
+ for (const [name, value] of [
55
+ ['XENON_MAX_CONCURRENT_RECORDINGS', env.max],
56
+ ['XENON_RECORDINGS_ASSETS_PATH', env.path],
57
+ ]) {
58
+ if (value === undefined)
59
+ delete process.env[name];
60
+ else
61
+ process.env[name] = value;
62
+ }
63
+ plugin_1.XenonPlugin.NODE_ID = statics.nodeId;
64
+ plugin_1.XenonPlugin.port = statics.port;
65
+ plugin_1.XenonPlugin.nodeBasePath = statics.basePath;
66
+ });
67
+ /**
68
+ * Boots as far as the database step and says what the options were when it
69
+ * began: the artifact store is built there from `recordingsAssetsPath`.
70
+ */
71
+ function seenAtDatabaseStep(pluginArgs) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ const seen = {};
74
+ const boot = ServerManager_1.ServerManager.prototype;
75
+ sinon_1.default.stub(boot, 'resolvePluginArgs').resolves(pluginArgs);
76
+ sinon_1.default.stub(logger_1.default, 'banner');
77
+ sinon_1.default.stub(PluginContext_1.PluginContext.prototype, 'setContext');
78
+ sinon_1.default.stub(boot, 'syncDatabaseAndAIConfig').resolves();
79
+ sinon_1.default.stub(boot, 'initializeCoreSubsystems').callsFake(() => __awaiter(this, void 0, void 0, function* () {
80
+ seen.max = config_1.config.maxConcurrentRecordings;
81
+ seen.path = config_1.config.recordingsAssetsPath;
82
+ throw new Error('boot stopped by the test');
83
+ }));
84
+ yield typedi_1.Container.get(ServerManager_1.ServerManager)
85
+ .updateServer({}, null, { port: 4723 })
86
+ .catch((err) => (0, chai_1.expect)(err.message).to.equal('boot stopped by the test'));
87
+ return seen;
88
+ });
89
+ }
90
+ describe('applied at boot', () => {
91
+ it('takes the maxConcurrentRecordings option', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: 2 });
93
+ (0, chai_1.expect)(seen.max).to.equal(2);
94
+ }));
95
+ it('takes the recordingsAssetsPath option', () => __awaiter(void 0, void 0, void 0, function* () {
96
+ const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: '/srv/xenon/recordings' });
97
+ (0, chai_1.expect)(seen.path).to.equal('/srv/xenon/recordings');
98
+ }));
99
+ it('falls back to the environment variables when the options are not set', () => __awaiter(void 0, void 0, void 0, function* () {
100
+ process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
101
+ process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
102
+ const seen = yield seenAtDatabaseStep({});
103
+ (0, chai_1.expect)(seen).to.deep.equal({ max: 7, path: '/mnt/recordings' });
104
+ }));
105
+ it('lets the option win over the environment variable', () => __awaiter(void 0, void 0, void 0, function* () {
106
+ process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
107
+ process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
108
+ const seen = yield seenAtDatabaseStep({
109
+ maxConcurrentRecordings: 3,
110
+ recordingsAssetsPath: '/srv/xenon/recordings',
111
+ });
112
+ (0, chai_1.expect)(seen).to.deep.equal({ max: 3, path: '/srv/xenon/recordings' });
113
+ }));
114
+ it('keeps the default cap of 4 when neither is set', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ const seen = yield seenAtDatabaseStep({});
116
+ (0, chai_1.expect)(seen.max).to.equal(4);
117
+ }));
118
+ it('ignores a cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ for (const bad of [0, -2, 2.5, NaN, '3', null]) {
120
+ const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: bad });
121
+ (0, chai_1.expect)(seen.max, `option ${JSON.stringify(bad)}`).to.equal(4);
122
+ sinon_1.default.restore();
123
+ }
124
+ }));
125
+ it('ignores an environment cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
126
+ process.env.XENON_MAX_CONCURRENT_RECORDINGS = 'many';
127
+ const seen = yield seenAtDatabaseStep({});
128
+ (0, chai_1.expect)(seen.max).to.equal(4);
129
+ }));
130
+ it('ignores a blank recordingsAssetsPath', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: ' ' });
132
+ (0, chai_1.expect)(seen.path).to.equal(saved.path);
133
+ }));
134
+ });
135
+ describe('the concurrency gate', () => {
136
+ it('reads the cap when it admits, not when it is built', () => {
137
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
138
+ const gate = new concurrency_gate_1.ConcurrencyGate();
139
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: 1 });
140
+ (0, chai_1.expect)(gate.getLimit()).to.equal(1);
141
+ (0, chai_1.expect)(gate.tryAcquire(['r1'])).to.equal(true);
142
+ (0, chai_1.expect)(gate.tryAcquire(['r2'])).to.equal(false);
143
+ });
144
+ it('keeps an explicit limit over the configured one', () => {
145
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
146
+ (0, chai_1.expect)(new concurrency_gate_1.ConcurrencyGate(2).getLimit()).to.equal(2);
147
+ });
148
+ });
149
+ });
150
+ describe('resolving the recording options', () => {
151
+ describe('maxConcurrentRecordings', () => {
152
+ it('takes the option, then the variable, then 4', () => {
153
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(2, '7')).to.equal(2);
154
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, '7')).to.equal(7);
155
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, undefined)).to.equal(4);
156
+ });
157
+ it('passes over a value that is not a whole number of at least 1', () => {
158
+ for (const bad of [0, -1, 1.5, NaN, Infinity, '2', null, {}]) {
159
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(bad, '7'), JSON.stringify(bad)).to.equal(7);
160
+ }
161
+ for (const bad of ['', ' ', '0', '-3', '2.5', 'many', 'NaN']) {
162
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, bad), JSON.stringify(bad)).to.equal(4);
163
+ }
164
+ });
165
+ });
166
+ describe('recordingsAssetsPath', () => {
167
+ it('takes the option, then the variable, then the default folder', () => {
168
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)('/a', '/b')).to.equal('/a');
169
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '/b')).to.equal('/b');
170
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, undefined)).to.match(/\.cache\/xenon\/assets\/sessions\/recordings$/);
171
+ });
172
+ it('counts a blank value as unset', () => {
173
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' ', '/b')).to.equal('/b');
174
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '')).to.match(/recordings$/);
175
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(42, undefined)).to.match(/recordings$/);
176
+ });
177
+ it('trims what it is given', () => {
178
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' /a ', undefined)).to.equal('/a');
179
+ });
180
+ });
181
+ });
@@ -2,10 +2,13 @@
2
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
+ var _a;
5
6
  Object.defineProperty(exports, "__esModule", { value: true });
6
7
  const chai_1 = require("chai");
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
10
+ // eslint-disable-next-line @typescript-eslint/no-var-requires -- a plain Node script's list
11
+ const { ALWAYS_PRESENT_PLUGIN_ARGS } = require('../../scripts/lib/always-present-plugin-args');
9
12
  // Adding a plugin arg to schema.json's `required` list is a BREAKING change for
10
13
  // every existing config file. Appium validates the config against this schema
11
14
  // and refuses to start when a required key is absent:
@@ -18,28 +21,30 @@ const path_1 = __importDefault(require("path"));
18
21
  // in 1.12.0 — an already-running server would not come back up after upgrading,
19
22
  // with no way to tell from the message that the fix is to edit a YAML file.
20
23
  //
21
- // An arg with a default must be optional: the schema default and the code's own
22
- // destructuring fallback already supply the value.
24
+ // An arg with a default must be optional: Appium fills in schema defaults after
25
+ // it validates the file, and the plugin merges DefaultPluginArgs as well. Until
26
+ // 2.13.1, 23 args were both required and defaulted, so a config file had to list
27
+ // all of them; even the README's five-option sample was refused. None is now.
23
28
  const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
24
- /** Args already both required and defaulted when this guard was added. */
25
- const KNOWN_REQUIRED_WITH_DEFAULT = 23;
29
+ const required = (_a = schema.required) !== null && _a !== void 0 ? _a : [];
26
30
  describe('schema.json required args', () => {
27
- it('does not grow the set of args that are required despite having a default', () => {
31
+ it('requires no arg that has a default', () => {
28
32
  // A default means "you may omit this", which is the opposite of required.
29
- // 23 args already sit in both lists; they only work because the Mac app
30
- // writes every required key into the config it generates, so a hand-written
31
- // or older config is already fragile against them. That is pre-existing
32
- // debt, not something to fix in a hotfix — but the set must not GROW,
33
- // because each addition breaks every config written before it.
34
- const contradictory = schema.required.filter((key) => schema.properties[key] && 'default' in schema.properties[key]);
35
- (0, chai_1.expect)(contradictory.length, `New required-with-default arg(s): ${contradictory.slice(KNOWN_REQUIRED_WITH_DEFAULT).join(', ')}. ` +
36
- 'An existing config that omits them fails Appium config validation and the server ' +
37
- 'will not start. Give the arg a default and leave it out of `required`.').to.equal(KNOWN_REQUIRED_WITH_DEFAULT);
33
+ const contradictory = required.filter((key) => schema.properties[key] && 'default' in schema.properties[key]);
34
+ (0, chai_1.expect)(contradictory, 'An existing config that omits these fails Appium config validation and the server ' +
35
+ 'will not start. Give the arg a default and leave it out of `required`.').to.deep.equal([]);
38
36
  });
39
37
  it('keeps every required arg present in properties', () => {
40
- const missing = schema.required.filter((key) => !schema.properties[key]);
38
+ const missing = required.filter((key) => !schema.properties[key]);
41
39
  (0, chai_1.expect)(missing, 'required lists an arg that does not exist').to.deep.equal([]);
42
40
  });
41
+ it('types as always present only args that have a default', () => {
42
+ // IPluginArgs keeps these fields non-optional (scripts/generate-types-from-schema.js
43
+ // marks them required for type generation only). That is true only while each
44
+ // exists and has a default for Appium and DefaultPluginArgs to fill in.
45
+ const wrong = ALWAYS_PRESENT_PLUGIN_ARGS.filter((key) => !schema.properties[key] || !('default' in schema.properties[key]));
46
+ (0, chai_1.expect)(wrong, 'listed as always present but missing or without a default').to.deep.equal([]);
47
+ });
43
48
  it('does not require the recording retention args', () => {
44
49
  // Named explicitly: this is the regression that took a live server down.
45
50
  for (const key of [
@@ -48,7 +53,7 @@ describe('schema.json required args', () => {
48
53
  'recordingFailedCleanupDays',
49
54
  ]) {
50
55
  (0, chai_1.expect)(schema.properties[key], `${key} should exist`).to.be.an('object');
51
- (0, chai_1.expect)(schema.required, `${key} must stay optional`).to.not.include(key);
56
+ (0, chai_1.expect)(required, `${key} must stay optional`).to.not.include(key);
52
57
  }
53
58
  });
54
59
  });