@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,249 @@
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 assetManager = __importStar(require("../../src/dashboard/asset-manager"));
53
+ const commands_1 = require("../../src/dashboard/commands");
54
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
55
+ const SocketServer_1 = require("../../src/services/SocketServer");
56
+ const container_registration_1 = require("../helpers/container-registration");
57
+ const scratch_database_1 = require("../helpers/scratch-database");
58
+ /**
59
+ * The five execute commands that write to a session's record:
60
+ * setSessionName, setSessionStatus, debug, addTag and captureEvidence.
61
+ *
62
+ * A session has a record (a Session row) only on a hub with its dashboard
63
+ * on. Elsewhere (the dashboard off, or on a node, which never keeps one for
64
+ * the hub's sessions) setSessionName and setSessionStatus threw Prisma's
65
+ * P2025 and debug a foreign-key error, failing the test's command, while
66
+ * addTag and captureEvidence answered as if they had worked.
67
+ *
68
+ * Each now answers `{ recorded: true }` when it wrote, or `{ recorded: false,
69
+ * message }` saying why nothing was written. None of them fails the test's
70
+ * command: they are bookkeeping, and a test shouldn't die because its server
71
+ * keeps no dashboard.
72
+ */
73
+ describe('xenon: session-details commands', () => {
74
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
75
+ const SESSION = 'session-details-1';
76
+ let restoreContainer;
77
+ let emitted;
78
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
79
+ yield scratch.db.log.deleteMany({});
80
+ yield scratch.db.sessionLog.deleteMany({});
81
+ yield scratch.db.session.deleteMany({});
82
+ restoreContainer = (0, container_registration_1.saveRegistrations)(SocketServer_1.SocketServer);
83
+ const socket = new SocketServer_1.SocketServer();
84
+ typedi_1.Container.set(SocketServer_1.SocketServer, socket);
85
+ emitted = sinon_1.default.stub(socket, 'emitToDashboardForDevices').resolves();
86
+ }));
87
+ afterEach(() => {
88
+ SessionManager_1.SESSION_MANAGER.removeSession(SESSION);
89
+ sinon_1.default.restore();
90
+ restoreContainer();
91
+ });
92
+ function withRecord() {
93
+ return __awaiter(this, void 0, void 0, function* () {
94
+ yield scratch.db.session.create({
95
+ data: {
96
+ id: SESSION,
97
+ desired_capabilities: '{}',
98
+ session_capabilities: '{}',
99
+ node_id: 'node-1',
100
+ has_live_video: false,
101
+ device_udid: 'phone-1',
102
+ device_platform: 'android',
103
+ device_version: '14',
104
+ },
105
+ });
106
+ });
107
+ }
108
+ /** What the command answers, as the local interceptor and the hub's gateway both see it. */
109
+ function run(script, args) {
110
+ return __awaiter(this, void 0, void 0, function* () {
111
+ let body;
112
+ const response = {
113
+ status: (code) => ({
114
+ json: (d) => {
115
+ body = Object.assign({ code }, d);
116
+ return d;
117
+ },
118
+ }),
119
+ };
120
+ yield commands_1.dashboardCommands.process(SESSION, { body: { script, args } }, response);
121
+ return body;
122
+ });
123
+ }
124
+ describe('which scripts are session-details commands', () => {
125
+ it('knows the five, under either prefix, and nothing else', () => {
126
+ (0, chai_1.expect)((0, commands_1.sessionDetailsCommandOf)('xenon: setSessionName')).to.equal('setSessionName');
127
+ (0, chai_1.expect)((0, commands_1.sessionDetailsCommandOf)('xe:addTag')).to.equal('addTag');
128
+ (0, chai_1.expect)((0, commands_1.sessionDetailsCommandOf)(' xenon : captureEvidence ')).to.equal('captureEvidence');
129
+ for (const other of [
130
+ 'xenon: smartTap',
131
+ 'xe: setAutowaitProperties',
132
+ 'xenon: addMock',
133
+ 'xenon: unknownThing',
134
+ 'mobile: shell',
135
+ 'xenonsetSessionName',
136
+ 'return 1',
137
+ undefined,
138
+ 42,
139
+ ]) {
140
+ (0, chai_1.expect)((0, commands_1.sessionDetailsCommandOf)(other), String(other)).to.equal(null);
141
+ }
142
+ });
143
+ });
144
+ describe('with no session record', () => {
145
+ const cases = [
146
+ ['xenon: setSessionName', ['Checkout']],
147
+ ['xenon: setSessionStatus', [{ status: 'passed' }]],
148
+ ['xenon: debug', ['reached checkout']],
149
+ ['xenon: addTag', ['smoke']],
150
+ ['xenon: captureEvidence', ['Payment confirmed']],
151
+ ];
152
+ for (const [script, args] of cases) {
153
+ it(`${script} answers that nothing was recorded, and writes nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
154
+ const body = yield run(script, args);
155
+ (0, chai_1.expect)(body.code).to.equal(200);
156
+ (0, chai_1.expect)(body.value).to.include({ recorded: false });
157
+ (0, chai_1.expect)(body.value.message).to.match(/no record/i);
158
+ (0, chai_1.expect)(yield scratch.db.log.count()).to.equal(0);
159
+ (0, chai_1.expect)(yield scratch.db.sessionLog.count()).to.equal(0);
160
+ }));
161
+ }
162
+ });
163
+ describe('with a session record', () => {
164
+ beforeEach(withRecord);
165
+ it('setSessionName names the session', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ var _a, _b;
167
+ (0, chai_1.expect)((yield run('xenon: setSessionName', ['Checkout'])).value).to.deep.equal({
168
+ recorded: true,
169
+ });
170
+ (0, chai_1.expect)((_a = (yield scratch.db.session.findUnique({ where: { id: SESSION } }))) === null || _a === void 0 ? void 0 : _a.name).to.equal('Checkout');
171
+ yield run('xe: setSessionName', [{ name: 'Checkout by card' }]);
172
+ (0, chai_1.expect)((_b = (yield scratch.db.session.findUnique({ where: { id: SESSION } }))) === null || _b === void 0 ? void 0 : _b.name).to.equal('Checkout by card');
173
+ }));
174
+ it('setSessionName without a name records nothing, and says so', () => __awaiter(void 0, void 0, void 0, function* () {
175
+ const body = yield run('xenon: setSessionName', []);
176
+ (0, chai_1.expect)(body.value).to.include({ recorded: false });
177
+ (0, chai_1.expect)(body.value.message).to.match(/name/);
178
+ }));
179
+ it('setSessionStatus sets the status and reason, and tells the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
180
+ var _a;
181
+ const body = yield run('xenon: setSessionStatus', [{ status: 'failed', reason: 'No total' }]);
182
+ (0, chai_1.expect)(body.value).to.deep.equal({ recorded: true });
183
+ const row = yield scratch.db.session.findUnique({ where: { id: SESSION } });
184
+ (0, chai_1.expect)(row).to.include({ status: 'failed', failure_reason: 'No total' });
185
+ (0, chai_1.expect)(emitted.firstCall.args[1]).to.include({
186
+ id: SESSION,
187
+ status: 'failed',
188
+ failure_reason: 'No total',
189
+ });
190
+ yield run('xenon: setSessionStatus', ['passed']);
191
+ (0, chai_1.expect)((_a = (yield scratch.db.session.findUnique({ where: { id: SESSION } }))) === null || _a === void 0 ? void 0 : _a.status).to.equal('success');
192
+ }));
193
+ it('setSessionStatus with a status it does not know records nothing, and says so', () => __awaiter(void 0, void 0, void 0, function* () {
194
+ var _a;
195
+ const body = yield run('xenon: setSessionStatus', [{ status: 'skipped' }]);
196
+ (0, chai_1.expect)(body.value).to.include({ recorded: false });
197
+ (0, chai_1.expect)(body.value.message).to.match(/skipped/);
198
+ (0, chai_1.expect)((_a = (yield scratch.db.session.findUnique({ where: { id: SESSION } }))) === null || _a === void 0 ? void 0 : _a.status).to.equal('running');
199
+ }));
200
+ it('debug adds a debug line', () => __awaiter(void 0, void 0, void 0, function* () {
201
+ (0, chai_1.expect)((yield run('xenon: debug', ['reached checkout'])).value).to.deep.equal({
202
+ recorded: true,
203
+ });
204
+ yield run('xenon: debug', [{ message: 'paid' }]);
205
+ const lines = yield scratch.db.log.findMany({ orderBy: { createdAt: 'asc' } });
206
+ (0, chai_1.expect)(lines.map((l) => [l.log_type, l.message])).to.deep.equal([
207
+ ['DEBUG', 'reached checkout'],
208
+ ['DEBUG', 'paid'],
209
+ ]);
210
+ }));
211
+ it('debug without a message records nothing, and says so', () => __awaiter(void 0, void 0, void 0, function* () {
212
+ const body = yield run('xenon: debug', []);
213
+ (0, chai_1.expect)(body.value).to.include({ recorded: false });
214
+ (0, chai_1.expect)(yield scratch.db.log.count()).to.equal(0);
215
+ }));
216
+ it('addTag adds a tag once', () => __awaiter(void 0, void 0, void 0, function* () {
217
+ (0, chai_1.expect)((yield run('xenon: addTag', ['smoke'])).value).to.deep.equal({ recorded: true });
218
+ (0, chai_1.expect)((yield run('xenon: addTag', [{ tag: 'smoke' }])).value).to.deep.equal({
219
+ recorded: true,
220
+ });
221
+ yield run('xenon: addTag', ['regression']);
222
+ const row = yield scratch.db.session.findUnique({ where: { id: SESSION } });
223
+ (0, chai_1.expect)(JSON.parse(String(row === null || row === void 0 ? void 0 : row.tags))).to.deep.equal(['smoke', 'regression']);
224
+ }));
225
+ it('captureEvidence records a screenshot of a session this server drives', () => __awaiter(void 0, void 0, void 0, function* () {
226
+ SessionManager_1.SESSION_MANAGER.addSession(SESSION, { getScreenShot: () => __awaiter(void 0, void 0, void 0, function* () { return 'aGVsbG8='; }) });
227
+ sinon_1.default.stub(assetManager, 'saveScreenShot').returns(`${SESSION}/screenshots/x.png`);
228
+ const body = yield run('xenon: captureEvidence', [{ reason: 'Paid', label: 'receipt' }]);
229
+ (0, chai_1.expect)(body.value).to.deep.equal({ recorded: true });
230
+ const rows = yield scratch.db.sessionLog.findMany({});
231
+ (0, chai_1.expect)(rows.map((r) => [r.command_name, r.subtitle, r.screenshot])).to.deep.equal([
232
+ ['captureEvidence', 'Paid', `${SESSION}/screenshots/x.png`],
233
+ ]);
234
+ }));
235
+ it("captureEvidence records nothing, and says so, when it can't take a screenshot", () => __awaiter(void 0, void 0, void 0, function* () {
236
+ let body = yield run('xenon: captureEvidence', ['Paid']);
237
+ (0, chai_1.expect)(body.value).to.include({ recorded: false });
238
+ SessionManager_1.SESSION_MANAGER.addSession(SESSION, {
239
+ getScreenShot: () => __awaiter(void 0, void 0, void 0, function* () {
240
+ throw new Error('WDA is not running');
241
+ }),
242
+ });
243
+ body = yield run('xenon: captureEvidence', ['Paid']);
244
+ (0, chai_1.expect)(body.value).to.include({ recorded: false });
245
+ (0, chai_1.expect)(body.value.message).to.match(/WDA is not running/);
246
+ (0, chai_1.expect)(yield scratch.db.sessionLog.count()).to.equal(0);
247
+ }));
248
+ });
249
+ });
@@ -253,6 +253,8 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
253
253
  delete teams[filter.udid];
254
254
  })),
255
255
  updateDevice: sinon_1.default.stub().resolves(),
256
+ // removeDevice reads the row for the device_offline webhook's name and platform.
257
+ findDevice: sinon_1.default.stub().resolves(null),
256
258
  getDevices: sinon_1.default.stub().resolves([
257
259
  {
258
260
  udid: 'phone-b',
@@ -308,13 +310,14 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
308
310
  }));
309
311
  });
310
312
  it("xenon: setSessionStatus: the updated session's device", () => __awaiter(void 0, void 0, void 0, function* () {
313
+ sinon_1.default.stub(prisma_1.prisma.session, 'findUnique').resolves({ id: 's3', tags: null });
311
314
  sinon_1.default.stub(prisma_1.prisma.session, 'update').resolves({ id: 's3', device_udid: 'phone-b' });
312
315
  const res = { status: () => res, json: () => res };
313
316
  yield new commands_1.DashboardCommands().process('s3', {
314
317
  body: { script: 'xenon: setSessionStatus', args: [{ status: 'failed', reason: 'boom' }] },
315
318
  }, res);
316
319
  const [call] = scoped(SocketEvents_1.SocketEvents.SESSION_STOPPED);
317
- (0, chai_1.expect)(call.args[1]).to.deep.equal({ id: 's3', status: 'failed', failure_reason: undefined });
320
+ (0, chai_1.expect)(call.args[1]).to.deep.equal({ id: 's3', status: 'failed', failure_reason: 'boom' });
318
321
  (0, chai_1.expect)(call.args[2]).to.deep.equal({ udid: 'phone-b' });
319
322
  }));
320
323
  describe('interceptor', () => {
@@ -400,11 +403,10 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
400
403
  }));
401
404
  it('a team change (PUT /device/:udid/team) refreshes the resolver at once', () => __awaiter(void 0, void 0, void 0, function* () {
402
405
  sinon_1.default.stub(prisma_1.prisma.team, 'findUnique').resolves({ id: 'team-c' });
403
- sinon_1.default.stub(prisma_1.prisma.device, 'updateMany').resolves({ count: 1 });
404
- sinon_1.default
405
- .stub(prisma_1.prisma.device, 'findMany')
406
- .resolves([{ udid: 'phone-a', host: 'h', teamId: 'team-c' }]);
407
- sinon_1.default.stub(device_store_1.DeviceStoreFactory.getStore(), 'updateDevice').resolves();
406
+ const store = device_store_1.DeviceStoreFactory.getStore();
407
+ sinon_1.default.stub(store, 'findDevices').resolves([{ udid: 'phone-a', host: 'h' }]);
408
+ sinon_1.default.stub(store, 'findSavedPhones').resolves([]);
409
+ const write = sinon_1.default.stub(store, 'updateDevice').resolves();
408
410
  const resolver = typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver);
409
411
  yield resolver.resolve('phone-a'); // cached as team-a
410
412
  const app = (0, express_1.default)();
@@ -424,6 +426,7 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
424
426
  app.use(router);
425
427
  const res = yield (0, loopbackRequest_1.default)(app).put('/device/phone-a/team').send({ teamId: 'team-c' });
426
428
  (0, chai_1.expect)(res.status).to.equal(200);
429
+ (0, chai_1.expect)(write.firstCall.args).to.deep.equal(['phone-a', 'h', { teamId: 'team-c' }]);
427
430
  (0, chai_1.expect)(yield resolver.resolve('phone-a')).to.deep.equal({ known: true, teamId: 'team-c' });
428
431
  (0, chai_1.expect)(lookups).to.deep.equal(['phone-a']);
429
432
  }));
@@ -0,0 +1,108 @@
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
+ const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
8
+ const config_1 = require("../../src/config");
9
+ const logger_1 = __importDefault(require("../../src/logger"));
10
+ const database_check_1 = require("../../src/scripts/database-check");
11
+ // The published plugin ships a database client generated for SQLite. Through
12
+ // 2.13.1, `databaseProvider: postgresql` with a SQLite file URL quietly ran on
13
+ // SQLite (Xenon Control profiles did exactly this), and a postgres URL stopped
14
+ // the server with Prisma's "the URL must start with the protocol `file:`" under
15
+ // a hint about adding a column to a table with rows.
16
+ describe('checkDatabase', () => {
17
+ const sqliteFile = 'file:/Users/me/.cache/xenon/xenon.db';
18
+ const postgresUrl = 'postgresql://xenon:s3cret@db.example.com:5432/xenon';
19
+ it('accepts a SQLite file with a SQLite client and says nothing', () => {
20
+ (0, chai_1.expect)((0, database_check_1.checkDatabase)({ configured: 'sqlite', url: sqliteFile, client: 'sqlite' })).to.deep.equal({});
21
+ });
22
+ it('runs a SQLite file on a SQLite client when configured for postgresql, with a warning', () => {
23
+ const { warning } = (0, database_check_1.checkDatabase)({
24
+ configured: 'postgresql',
25
+ url: sqliteFile,
26
+ client: 'sqlite',
27
+ });
28
+ (0, chai_1.expect)(warning).to.be.a('string');
29
+ (0, chai_1.expect)(warning).to.match(/databaseProvider/);
30
+ (0, chai_1.expect)(warning).to.match(/SQLite/);
31
+ (0, chai_1.expect)(warning).to.include('/Users/me/.cache/xenon/xenon.db');
32
+ });
33
+ it('refuses a postgres URL when the client is built for SQLite, without printing the password', () => {
34
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'postgresql', url: postgresUrl, client: 'sqlite' }))
35
+ .to.throw(/SQLite/)
36
+ .with.property('message')
37
+ .that.does.not.include('s3cret');
38
+ });
39
+ it('refuses a postgres URL even when databaseProvider says sqlite', () => {
40
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'sqlite', url: postgresUrl, client: 'sqlite' })).to.throw(/db\.example\.com/);
41
+ });
42
+ it('accepts a postgres URL when the client was generated for PostgreSQL', () => {
43
+ (0, chai_1.expect)((0, database_check_1.checkDatabase)({ configured: 'postgresql', url: postgresUrl, client: 'postgresql' })).to.deep.equal({});
44
+ });
45
+ it('refuses a SQLite file when the client was generated for PostgreSQL', () => {
46
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'postgresql', url: sqliteFile, client: 'postgresql' })).to.throw(/PostgreSQL/);
47
+ });
48
+ it('refuses a URL it cannot place', () => {
49
+ (0, chai_1.expect)(() => (0, database_check_1.checkDatabase)({ configured: 'sqlite', url: 'mysql://h/db', client: 'sqlite' })).to.throw(/mysql:/);
50
+ });
51
+ });
52
+ describe('clientProviderFromSchema', () => {
53
+ it("reads the datasource's provider, not the generator's", () => {
54
+ const schema = [
55
+ 'generator client {',
56
+ ' provider = "prisma-client-js"',
57
+ '}',
58
+ '',
59
+ 'datasource db {',
60
+ ' provider = "postgresql"',
61
+ ' url = env("DATABASE_URL")',
62
+ '}',
63
+ ].join('\n');
64
+ (0, chai_1.expect)((0, database_check_1.clientProviderFromSchema)(schema)).to.equal('postgresql');
65
+ });
66
+ it('returns null when there is no datasource', () => {
67
+ (0, chai_1.expect)((0, database_check_1.clientProviderFromSchema)('generator client {}')).to.equal(null);
68
+ });
69
+ });
70
+ describe('describeDatabaseUrl', () => {
71
+ it('keeps a SQLite path and drops credentials from a server URL', () => {
72
+ (0, chai_1.expect)((0, database_check_1.describeDatabaseUrl)('file:/data/xenon.db')).to.equal('/data/xenon.db');
73
+ (0, chai_1.expect)((0, database_check_1.describeDatabaseUrl)('postgresql://u:p@h:5432/db?schema=x')).to.equal('postgresql://h:5432/db');
74
+ });
75
+ });
76
+ describe('assertSupportedDatabase', () => {
77
+ let saved;
78
+ beforeEach(() => {
79
+ saved = { provider: config_1.config.databaseProvider, url: config_1.config.databaseUrl };
80
+ });
81
+ afterEach(() => {
82
+ config_1.config.databaseProvider = saved.provider;
83
+ config_1.config.databaseUrl = saved.url;
84
+ sinon_1.default.restore();
85
+ });
86
+ // These read the generated client this repo ships (src/generated/client),
87
+ // which is built for SQLite.
88
+ it('stops a server whose URL is a postgres database', () => {
89
+ config_1.config.databaseProvider = 'postgresql';
90
+ config_1.config.databaseUrl = 'postgresql://xenon:pw@127.0.0.1:5432/xenon';
91
+ (0, chai_1.expect)(() => (0, database_check_1.assertSupportedDatabase)()).to.throw(/built for SQLite/);
92
+ });
93
+ it('starts a postgresql-labelled server on its SQLite file, and says so once', () => {
94
+ const warn = sinon_1.default.stub(logger_1.default, 'warn');
95
+ config_1.config.databaseProvider = 'postgresql';
96
+ config_1.config.databaseUrl = 'file:/tmp/xenon-check.db';
97
+ (0, database_check_1.assertSupportedDatabase)();
98
+ (0, chai_1.expect)(warn.callCount).to.equal(1);
99
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('/tmp/xenon-check.db');
100
+ });
101
+ it('is silent for the default SQLite setup', () => {
102
+ const warn = sinon_1.default.stub(logger_1.default, 'warn');
103
+ config_1.config.databaseProvider = 'sqlite';
104
+ config_1.config.databaseUrl = 'file:/tmp/xenon-check.db';
105
+ (0, database_check_1.assertSupportedDatabase)();
106
+ (0, chai_1.expect)(warn.called).to.equal(false);
107
+ });
108
+ });
@@ -0,0 +1,33 @@
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
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
10
+ const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
11
+ /**
12
+ * Appium fills every default schema.json declares, so a server started the
13
+ * normal way runs on those. DefaultPluginArgs (the block in
14
+ * scripts/generate-types-from-schema.js) is the copy the plugin merges under
15
+ * its options, and used to say 86400000 ms for the health check while the
16
+ * schema, and so the running server, said 300000. A second copy that can
17
+ * disagree is how the Settings page came to show a third number.
18
+ */
19
+ describe('DefaultPluginArgs', () => {
20
+ it('agrees with every default schema.json declares', () => {
21
+ const disagreements = Object.entries(schema.properties)
22
+ .filter(([key, prop]) => 'default' in prop && key in IPluginArgs_1.DefaultPluginArgs)
23
+ .filter(([key, prop]) => IPluginArgs_1.DefaultPluginArgs[key] !== undefined &&
24
+ JSON.stringify(IPluginArgs_1.DefaultPluginArgs[key]) !==
25
+ JSON.stringify(prop.default))
26
+ .map(([key, prop]) => ({
27
+ key,
28
+ schema: prop.default,
29
+ defaultPluginArgs: IPluginArgs_1.DefaultPluginArgs[key],
30
+ }));
31
+ (0, chai_1.expect)(disagreements).to.deep.equal([]);
32
+ });
33
+ });