@xenon-device-management/xenon 2.13.2 → 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 (209) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
  40. package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
  41. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
  42. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
  43. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
  44. package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
  45. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  46. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  48. package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
  49. package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
  50. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
  51. package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
  52. package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
  53. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
  54. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
  56. package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
  59. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  60. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  61. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
  62. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
  63. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
  64. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
  65. package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  68. package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
  69. package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
  70. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
  71. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  72. package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +19 -21
  75. package/lib/src/XenonCapabilityManager.js +22 -5
  76. package/lib/src/app/openapi/control.yaml +71 -20
  77. package/lib/src/app/openapi/grid.yaml +23 -5
  78. package/lib/src/app/openapi/platform.yaml +136 -28
  79. package/lib/src/app/routers/control.js +12 -2
  80. package/lib/src/app/routers/dashboard.js +29 -1
  81. package/lib/src/app/routers/grid.js +32 -10
  82. package/lib/src/app/routers/streamType.js +7 -2
  83. package/lib/src/app/routers/webhook.js +8 -4
  84. package/lib/src/config.js +50 -5
  85. package/lib/src/dashboard/commands.js +185 -141
  86. package/lib/src/dashboard/event-manager.js +39 -6
  87. package/lib/src/data-service/appleFamily.js +43 -0
  88. package/lib/src/data-service/db.js +11 -0
  89. package/lib/src/data-service/device-service.js +6 -1
  90. package/lib/src/data-service/device-store.js +62 -2
  91. package/lib/src/data-service/deviceClaims.js +37 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  93. package/lib/src/data-service/deviceSettings.js +79 -0
  94. package/lib/src/data-service/prisma-store.js +150 -8
  95. package/lib/src/data-service/web-config-service.js +43 -92
  96. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  97. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  98. package/lib/src/device-managers/shellCommands.js +96 -0
  99. package/lib/src/device-utils.js +96 -40
  100. package/lib/src/generated/client/edge.js +17 -3
  101. package/lib/src/generated/client/index-browser.js +14 -0
  102. package/lib/src/generated/client/index.d.ts +2054 -379
  103. package/lib/src/generated/client/index.js +17 -3
  104. package/lib/src/generated/client/package.json +1 -1
  105. package/lib/src/generated/client/schema.prisma +35 -7
  106. package/lib/src/generated/client/wasm.js +14 -0
  107. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  108. package/lib/src/interceptors/xenonScripts.js +55 -0
  109. package/lib/src/interfaces/IPluginArgs.js +1 -2
  110. package/lib/src/plugin.js +15 -1
  111. package/lib/src/prisma.js +1 -1
  112. package/lib/src/scripts/database-check.js +124 -0
  113. package/lib/src/services/AIService.js +99 -0
  114. package/lib/src/services/CleanupService.js +6 -1
  115. package/lib/src/services/InterceptorService.js +70 -13
  116. package/lib/src/services/NetworkConditioningService.js +85 -88
  117. package/lib/src/services/NotificationService.js +36 -29
  118. package/lib/src/services/OrphanSweeper.js +5 -0
  119. package/lib/src/services/PortAllocator.js +5 -0
  120. package/lib/src/services/ServerManager.js +94 -8
  121. package/lib/src/services/SessionLifecycleService.js +47 -49
  122. package/lib/src/services/ShutdownCoordinator.js +33 -0
  123. package/lib/src/services/TeamService.js +20 -3
  124. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  125. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  126. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  127. package/lib/src/services/network/AndroidRadios.js +71 -0
  128. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  129. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  130. package/lib/src/services/network/adbForPhone.js +60 -0
  131. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  132. package/lib/src/services/network/xenonProxy.js +107 -0
  133. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  134. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  135. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  136. package/lib/src/services/omni-vision/screenScale.js +68 -0
  137. package/lib/src/services/recording/concurrency-gate.js +12 -3
  138. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  139. package/lib/src/services/settings/labSettings.js +149 -0
  140. package/lib/src/services/webhookEvents.js +173 -0
  141. package/lib/test/helpers/fake-adb.js +58 -0
  142. package/lib/test/helpers/fake-device-table.js +63 -24
  143. package/lib/test/helpers/scratch-database.js +1 -0
  144. package/lib/test/unit/TeamService.test.js +3 -0
  145. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  146. package/lib/test/unit/apple-family.spec.js +36 -0
  147. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  148. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  149. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  150. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  151. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  152. package/lib/test/unit/database-check.spec.js +108 -0
  153. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  154. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  155. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  156. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  157. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  158. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  159. package/lib/test/unit/emulator-boot.spec.js +91 -0
  160. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  161. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  162. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  163. package/lib/test/unit/json-logging-option.spec.js +74 -0
  164. package/lib/test/unit/lab-settings.spec.js +218 -0
  165. package/lib/test/unit/max-sessions.spec.js +240 -0
  166. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  167. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  168. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  169. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  170. package/lib/test/unit/omni-vision.spec.js +1 -1
  171. package/lib/test/unit/partial-config-file.spec.js +14 -0
  172. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  173. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  174. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  175. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  176. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  177. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  178. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  179. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  180. package/lib/test/unit/plugin.spec.js +3 -3
  181. package/lib/test/unit/recording-options.spec.js +181 -0
  182. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  183. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  184. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  185. package/lib/test/unit/shell-commands.spec.js +133 -0
  186. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  187. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  188. package/lib/test/unit/stream-status-type.spec.js +4 -0
  189. package/lib/test/unit/vision-assertion.spec.js +118 -0
  190. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  191. package/lib/test/unit/web-config-service.spec.js +79 -0
  192. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  193. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  194. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  197. package/package.json +1 -1
  198. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  199. package/prisma/schema.prisma +35 -7
  200. package/schema.json +19 -21
  201. package/scripts/generate-types-from-schema.js +1 -2
  202. package/scripts/lib/always-present-plugin-args.js +4 -1
  203. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  204. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  205. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  206. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  207. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  208. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  209. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "prisma-client-ef3fbf8faed5c4c712a2dce2686e5afc3ad0aa576aedb9c4cd9e5c99cfc0f03c",
2
+ "name": "prisma-client-ec1ae6fb328d1bc102086acd1f77e77477081d17b653d7f5092f083d0c8a4d34",
3
3
  "main": "index.js",
4
4
  "types": "index.d.ts",
5
5
  "browser": "index-browser.js",
@@ -242,6 +242,33 @@ model Device {
242
242
  @@index([claimSessionId])
243
243
  }
244
244
 
245
+ /// The settings people give one phone (an admin's team, tags and maintenance,
246
+ /// a member's reservation), kept apart from its Device row. That row is
247
+ /// deleted whenever the phone goes: unplugged, rebooted, offline in adb, a
248
+ /// restart, its node gone. This one stays, and a new row for the phone starts
249
+ /// from it, in the same write (src/data-service/deviceSettings.ts). Keyed as
250
+ /// Device is: a phone is a udid on the server that drives it, so another
251
+ /// server's phone with the same udid (an emulator's) never takes these. A hub
252
+ /// keeps the settings of its nodes' phones here too; a node's report never
253
+ /// writes them.
254
+ model DeviceSetting {
255
+ udid String
256
+ host String
257
+ teamId String?
258
+ team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)
259
+ /// JSON array, as Device.tags.
260
+ tags String?
261
+ userBlocked Boolean @default(false)
262
+ reservationReason String?
263
+ reservedBy String?
264
+ reservedByUserId String?
265
+ reservedUntil Float?
266
+ updatedAt DateTime @updatedAt
267
+
268
+ @@id([udid, host])
269
+ @@index([teamId])
270
+ }
271
+
245
272
  model PendingSession {
246
273
  id Int @id @default(autoincrement())
247
274
  capability_id String @unique
@@ -348,13 +375,14 @@ model ApiKey {
348
375
  }
349
376
 
350
377
  model Team {
351
- id String @id @default(uuid())
352
- name String @unique
353
- createdAt DateTime @default(now())
354
- devices Device[]
355
- apiKeys ApiKey[]
356
- members TeamMember[]
357
- apps App[]
378
+ id String @id @default(uuid())
379
+ name String @unique
380
+ createdAt DateTime @default(now())
381
+ devices Device[]
382
+ deviceSettings DeviceSetting[]
383
+ apiKeys ApiKey[]
384
+ members TeamMember[]
385
+ apps App[]
358
386
  }
359
387
 
360
388
  model SelectorState {
@@ -302,6 +302,19 @@ exports.Prisma.DeviceScalarFieldEnum = {
302
302
  nodeHold: 'nodeHold'
303
303
  };
304
304
 
305
+ exports.Prisma.DeviceSettingScalarFieldEnum = {
306
+ udid: 'udid',
307
+ host: 'host',
308
+ teamId: 'teamId',
309
+ tags: 'tags',
310
+ userBlocked: 'userBlocked',
311
+ reservationReason: 'reservationReason',
312
+ reservedBy: 'reservedBy',
313
+ reservedByUserId: 'reservedByUserId',
314
+ reservedUntil: 'reservedUntil',
315
+ updatedAt: 'updatedAt'
316
+ };
317
+
305
318
  exports.Prisma.PendingSessionScalarFieldEnum = {
306
319
  id: 'id',
307
320
  capability_id: 'capability_id',
@@ -536,6 +549,7 @@ exports.Prisma.ModelName = {
536
549
  SessionMetric: 'SessionMetric',
537
550
  App: 'App',
538
551
  Device: 'Device',
552
+ DeviceSetting: 'DeviceSetting',
539
553
  PendingSession: 'PendingSession',
540
554
  CLIArgs: 'CLIArgs',
541
555
  WebhookConfig: 'WebhookConfig',
@@ -65,6 +65,8 @@ const sessionContext_1 = require("../logging/sessionContext");
65
65
  const ProcessMetricsService_1 = require("../services/ProcessMetricsService");
66
66
  const AutowaitService_1 = require("../services/autowait/AutowaitService");
67
67
  const waitFor_1 = require("../services/autowait/waitFor");
68
+ const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
69
+ const xenonScripts_1 = require("./xenonScripts");
68
70
  let CommandInterceptor = class CommandInterceptor {
69
71
  constructor() {
70
72
  this.log = logger_1.default.scope('CommandInterceptor');
@@ -182,10 +184,14 @@ let CommandInterceptor = class CommandInterceptor {
182
184
  }
183
185
  if (aiCommand === 'assertVisualState') {
184
186
  this.log.info(`[Interceptor] Routing AI command: ${script} with payload: ${JSON.stringify(scriptArgs)}`);
185
- const instruction = typeof scriptArgs === 'string'
186
- ? scriptArgs
187
- : typeof scriptArgs === 'object' && (scriptArgs === null || scriptArgs === void 0 ? void 0 : scriptArgs.instruction)
188
- ? scriptArgs.instruction
187
+ // The condition as a plain string (`execute(script, 'The cart is
188
+ // empty')`, which arrives as ['The cart is empty']) or as
189
+ // { instruction }. A plain string used to arrive as ''.
190
+ const first = Array.isArray(scriptArgs) ? scriptArgs[0] : scriptArgs;
191
+ const instruction = typeof first === 'string'
192
+ ? first
193
+ : typeof first === 'object' && typeof (first === null || first === void 0 ? void 0 : first.instruction) === 'string'
194
+ ? first.instruction
189
195
  : '';
190
196
  return yield typedi_1.Container.get(AICommandService_1.AICommandService).assertVisualState(driver, instruction);
191
197
  }
@@ -216,13 +222,28 @@ let CommandInterceptor = class CommandInterceptor {
216
222
  }
217
223
  }
218
224
  }
225
+ // A session-details command (`xenon: setSessionName`, ...) is answered
226
+ // by the before-hook; its answer is what the test gets.
227
+ let answered = null;
219
228
  const shouldProceed = yield event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, commandName, { body: { script: args[0], args: args[1] } }, {
220
- status: () => ({ json: (d) => d }),
229
+ status: () => ({
230
+ json: (d) => {
231
+ var _a;
232
+ answered = (_a = d === null || d === void 0 ? void 0 : d.value) !== null && _a !== void 0 ? _a : null;
233
+ return d;
234
+ },
235
+ }),
221
236
  setHeader: () => { },
222
237
  getHeader: () => { },
223
238
  });
224
239
  if (shouldProceed === false) {
225
- return null;
240
+ return answered;
241
+ }
242
+ // Every Xenon script this server has was answered above. Any other
243
+ // name used to be answered with null, as if it had worked.
244
+ if (commandName === 'execute' && (0, xenonScripts_1.xenonScriptName)(args[0]) !== null) {
245
+ const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
246
+ throw new errors.UnknownCommandError((0, xenonScripts_1.unknownXenonScriptMessage)(args[0]));
226
247
  }
227
248
  }
228
249
  // --- OMNI-VISION: PROACTIVE SEARCH ---
@@ -288,7 +309,7 @@ let CommandInterceptor = class CommandInterceptor {
288
309
  catch (error) {
289
310
  if (this.isNoSuchElementError(error) &&
290
311
  ['findElement', 'findElements'].includes(commandName) &&
291
- pluginArgs.enableSelfHealing !== false) {
312
+ typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
292
313
  // §2.7 healing-tier capability gate: a session created with
293
314
  // xe:options.healingTiers (or the xenon:options alias) restricts
294
315
  // self-healing to those tier indices (1=Resilio, 2=Fuzzy XML, 3=OCR,
@@ -456,7 +477,7 @@ let CommandInterceptor = class CommandInterceptor {
456
477
  }, {}, JSON.stringify({ value: response, sessionId }));
457
478
  if (commandName === 'findElement' &&
458
479
  response &&
459
- pluginArgs.enableSelfHealing !== false) {
480
+ typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
460
481
  this.triggerLearning(driver, args, response, sessionId);
461
482
  }
462
483
  }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * Xenon's execute scripts: `driver.execute('xenon: <name>', ...)`, or `xe:`.
4
+ *
5
+ * Who answers them:
6
+ * - The five session-details commands (SESSION_DETAILS_COMMANDS in
7
+ * dashboard/commands.ts) are answered by the server that keeps the
8
+ * session's record: a hub, for a session on a node's phone, when its
9
+ * dashboard is on (its gateway's before-hook), otherwise the server that
10
+ * drives the phone.
11
+ * - Every other one is answered by the server that drives the phone
12
+ * (CommandInterceptor), because that is where the session's autowait
13
+ * settings, Omni-Vision and network capture live. A hub forwards them.
14
+ * - A name Xenon doesn't have fails with `unknown command`.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.XENON_SCRIPT_NAMES = void 0;
18
+ exports.xenonScriptName = xenonScriptName;
19
+ exports.unknownXenonScriptMessage = unknownXenonScriptMessage;
20
+ /** The names CommandInterceptor and the session-details commands answer. Keep in step with both. */
21
+ exports.XENON_SCRIPT_NAMES = [
22
+ 'setAutowaitProperties',
23
+ 'getAutowaitProperties',
24
+ 'smartTap',
25
+ 'omniClick',
26
+ 'visualTap',
27
+ 'uiInventory',
28
+ 'uiScanExport',
29
+ 'analyzeScreen',
30
+ 'omniScan',
31
+ 'assertVisualState',
32
+ 'addMock',
33
+ 'removeMock',
34
+ 'clearMocks',
35
+ 'getMocks',
36
+ 'getRequests',
37
+ 'exportHar',
38
+ 'setSessionName',
39
+ 'setSessionStatus',
40
+ 'debug',
41
+ 'addTag',
42
+ 'captureEvidence',
43
+ ];
44
+ /** The name in a `xenon: <name>` or `xe: <name>` script, or null for any other script. */
45
+ function xenonScriptName(script) {
46
+ if (typeof script !== 'string')
47
+ return null;
48
+ const match = /^\s*(?:xenon|xe)\s*:\s*(\S+)\s*$/.exec(script);
49
+ return match ? match[1] : null;
50
+ }
51
+ /** The message for a `xenon:` / `xe:` script Xenon doesn't have. */
52
+ function unknownXenonScriptMessage(script) {
53
+ return (`Unknown Xenon command "${script.trim()}". ` +
54
+ `Xenon's execute commands are: ${exports.XENON_SCRIPT_NAMES.join(', ')}.`);
55
+ }
@@ -30,7 +30,7 @@ exports.DefaultPluginArgs = {
30
30
  bindHostOrIp: 'auto',
31
31
  enableDashboard: false,
32
32
  bootedSimulators: false,
33
- healthCheckIntervalMs: 86400000,
33
+ healthCheckIntervalMs: 300000,
34
34
  healthCheckSchedule: undefined,
35
35
  removeDevicesFromDatabaseBeforeRunningThePlugin: false,
36
36
  databaseProvider: undefined,
@@ -50,7 +50,6 @@ exports.DefaultPluginArgs = {
50
50
  recordingCleanupMaxCount: 100,
51
51
  recordingFailedCleanupDays: 2,
52
52
  sessionHeartbeatIntervalMs: 30000,
53
- enableJsonLogging: false,
54
53
  autowait: {
55
54
  enabled: false,
56
55
  timeoutMs: 10000,
package/lib/src/plugin.js CHANGED
@@ -67,6 +67,7 @@ const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
67
67
  const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
68
68
  const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
69
69
  const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
70
+ const PhoneNetworkRestore_1 = require("./services/network/PhoneNetworkRestore");
70
71
  const config_1 = require("./config");
71
72
  const SessionManager_1 = require("./sessions/SessionManager");
72
73
  const event_manager_1 = require("./dashboard/event-manager");
@@ -75,13 +76,22 @@ const session_service_1 = require("./dashboard/services/session-service");
75
76
  const DEVICE_MANAGER_LOCK_NAME = 'DeviceManager';
76
77
  class XenonPlugin extends base_plugin_1.default {
77
78
  constructor(pluginName, cliArgs) {
79
+ var _a;
78
80
  super(pluginName, cliArgs);
79
81
  this.xenonLog = logger_1.default.scope('Plugin');
80
82
  this.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs);
81
83
  this.aiCommandService = typedi_1.Container.get(AICommandService_1.AICommandService);
82
84
  this.xenonLog.debug(`📱 Plugin Args: ${JSON.stringify(cliArgs)}`);
83
85
  this.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, this.cliArgs);
84
- logger_1.XenonLogger.configure({ enableJsonLogging: this.pluginArgs.enableJsonLogging });
86
+ // The option as it was given, not the merged copy above: JSON logging is on
87
+ // when the option says so, or when it isn't given and XENON_JSON_LOGGING does.
88
+ // schema.json gives the option no default, so Appium leaves it out when
89
+ // nobody set it; a default here (the merge above used to add `false`) would
90
+ // overwrite the variable on every start. `configure` ignores undefined.
91
+ const jsonLogging = (_a = this.cliArgs) === null || _a === void 0 ? void 0 : _a.enableJsonLogging;
92
+ logger_1.XenonLogger.configure({
93
+ enableJsonLogging: typeof jsonLogging === 'boolean' ? jsonLogging : undefined,
94
+ });
85
95
  if ((0, networkAddresses_1.shouldAutoResolveBindHost)(this.pluginArgs.bindHostOrIp)) {
86
96
  this.pluginArgs.bindHostOrIp = (0, networkAddresses_1.resolveAdvertisedBindHost)(this.pluginArgs.bindHostOrIp);
87
97
  }
@@ -101,6 +111,10 @@ class XenonPlugin extends base_plugin_1.default {
101
111
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
102
112
  if (sessionId)
103
113
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
114
+ // Appium's new-command timeout ends a session here, not in deleteSession:
115
+ // the phone's network (profile, interceptor proxy) and the capture are
116
+ // put back before the phone is released.
117
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, 'driver shut down');
104
118
  const deviceFilter = {
105
119
  session_id: sessionId ? sessionId : undefined,
106
120
  udid: driver.caps && driver.caps.udid ? driver.caps.udid : undefined,
package/lib/src/prisma.js CHANGED
@@ -33,7 +33,7 @@ exports.getPrismaClient = getPrismaClient;
33
33
  * can stub individual methods (Prisma's internal Proxy blocks property assignment).
34
34
  */
35
35
  const MODEL_DELEGATES = new Set([
36
- 'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device',
36
+ 'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device', 'deviceSetting',
37
37
  'pendingSession', 'cLIArgs', 'webhookConfig', 'webConfig', 'locatorEtalon',
38
38
  'lease', 'portLease', 'apiKey', 'selectorState', 'user', 'userSession',
39
39
  'passwordResetToken', 'team', 'teamMember', 'eventLog', 'project', 'recording', 'bookmark',
@@ -0,0 +1,124 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.clientProviderFromSchema = clientProviderFromSchema;
40
+ exports.describeDatabaseUrl = describeDatabaseUrl;
41
+ exports.checkDatabase = checkDatabase;
42
+ exports.assertSupportedDatabase = assertSupportedDatabase;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const logger_1 = __importDefault(require("../logger"));
46
+ const config_1 = require("../config");
47
+ const LABEL = { sqlite: 'SQLite', postgresql: 'PostgreSQL' };
48
+ function providerOfUrl(url) {
49
+ if (url.startsWith('file:'))
50
+ return 'sqlite';
51
+ if (/^postgres(ql)?:\/\//.test(url))
52
+ return 'postgresql';
53
+ return null;
54
+ }
55
+ /** The datasource provider in a Prisma schema's text, or null if it has none. */
56
+ function clientProviderFromSchema(schema) {
57
+ const match = /datasource\s+\w+\s*{[^}]*?provider\s*=\s*"([^"]+)"/.exec(schema);
58
+ return match ? match[1] : null;
59
+ }
60
+ /** A database URL fit for a log line: a SQLite file's path, or a server URL without credentials. */
61
+ function describeDatabaseUrl(url) {
62
+ if (url.startsWith('file:'))
63
+ return url.slice('file:'.length);
64
+ try {
65
+ const parsed = new URL(url);
66
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
67
+ }
68
+ catch (_a) {
69
+ return url.split(':')[0] + ':';
70
+ }
71
+ }
72
+ /**
73
+ * Decide whether the server may start on `url`. Throws an Error whose message
74
+ * says what to change; returns a warning when `configured` disagrees with the
75
+ * client but the URL is usable.
76
+ */
77
+ function checkDatabase(opts) {
78
+ var _a;
79
+ const { configured, url, client } = opts;
80
+ const clientLabel = (_a = LABEL[client]) !== null && _a !== void 0 ? _a : client;
81
+ const urlProvider = providerOfUrl(url);
82
+ const where = describeDatabaseUrl(url);
83
+ if (urlProvider !== client) {
84
+ const kind = urlProvider
85
+ ? `a ${LABEL[urlProvider]} database`
86
+ : 'not a database this install knows';
87
+ const fix = client === 'sqlite'
88
+ ? 'Set DATABASE_URL (or databaseUrl) to a SQLite file, `file:/path/to/xenon.db`, or leave it ' +
89
+ 'unset to use the default under ~/.cache/xenon. The published plugin stores its data in SQLite only.'
90
+ : `Set DATABASE_URL (or databaseUrl) to a ${clientLabel} URL.`;
91
+ throw new Error(`[Database] Cannot start: the database URL (${where}) is ${kind}, but this install's ` +
92
+ `database client is built for ${clientLabel}. ${fix}`);
93
+ }
94
+ if (configured && configured !== client) {
95
+ return {
96
+ warning: `[Database] databaseProvider is "${configured}", but this install's database client is ` +
97
+ `built for ${clientLabel}, so the setting has no effect. Your data is in ${where}. ` +
98
+ `Set databaseProvider to "${client}", or remove it.`,
99
+ };
100
+ }
101
+ return {};
102
+ }
103
+ /** The provider the generated client next to this file was built for. */
104
+ function generatedClientProvider() {
105
+ var _a;
106
+ const schemaPath = path.join(__dirname, '..', 'generated', 'client', 'schema.prisma');
107
+ try {
108
+ return (_a = clientProviderFromSchema(fs.readFileSync(schemaPath, 'utf8'))) !== null && _a !== void 0 ? _a : 'sqlite';
109
+ }
110
+ catch (_b) {
111
+ // The published plugin's client is SQLite; a missing copy means a plain install.
112
+ return 'sqlite';
113
+ }
114
+ }
115
+ /** Startup gate: log the warning, or throw so the server doesn't start on the wrong database. */
116
+ function assertSupportedDatabase() {
117
+ const { warning } = checkDatabase({
118
+ configured: config_1.config.databaseProvider,
119
+ url: config_1.config.databaseUrl,
120
+ client: generatedClientProvider(),
121
+ });
122
+ if (warning)
123
+ logger_1.default.warn(warning);
124
+ }
@@ -46,6 +46,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.AI_SERVICE = exports.AIService = void 0;
49
+ exports.parseVisualVerdict = parseVisualVerdict;
49
50
  const generative_ai_1 = require("@google/generative-ai");
50
51
  const openai_1 = __importDefault(require("openai"));
51
52
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
@@ -55,6 +56,41 @@ const fs = __importStar(require("fs"));
55
56
  const path = __importStar(require("path"));
56
57
  const config_1 = require("../config");
57
58
  const CircuitBreaker_1 = require("./CircuitBreaker");
59
+ /** What GeminiProvider answers, instead of throwing, when the provider is rate-limited (429). */
60
+ const RATE_LIMITED = 'CONNECTION_OK_RATE_LIMITED';
61
+ /**
62
+ * A provider's answer to the assertion prompt, `{ "result": true|false,
63
+ * "reason": "..." }`, read leniently (code fences, text around the JSON,
64
+ * `message` for `reason`, "true"/"false" as strings) but never guessed: null
65
+ * when it holds no true/false `result`.
66
+ */
67
+ function parseVisualVerdict(answer) {
68
+ const text = answer.replace(/```(?:json)?/gi, '').trim();
69
+ const start = text.indexOf('{');
70
+ const end = text.lastIndexOf('}');
71
+ if (start === -1 || end <= start)
72
+ return null;
73
+ let data;
74
+ try {
75
+ data = JSON.parse(text.slice(start, end + 1));
76
+ }
77
+ catch (_a) {
78
+ return null;
79
+ }
80
+ const raw = data === null || data === void 0 ? void 0 : data.result;
81
+ const result = raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
82
+ if (result === null)
83
+ return null;
84
+ const reason = typeof data.reason === 'string'
85
+ ? data.reason
86
+ : typeof data.message === 'string'
87
+ ? data.message
88
+ : '';
89
+ return {
90
+ result,
91
+ message: reason.trim() || (result ? 'The condition holds.' : 'The condition does not hold.'),
92
+ };
93
+ }
58
94
  class GeminiProvider {
59
95
  constructor(apiKey, modelName) {
60
96
  this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey.trim());
@@ -365,6 +401,69 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
365
401
  }
366
402
  });
367
403
  }
404
+ /**
405
+ * Asks the provider whether `condition` holds on the screenshot
406
+ * (`xenon: assertVisualState`). Answers only with the provider's own
407
+ * verdict: with no provider, a failed call, a rate limit or an answer that
408
+ * isn't true or false it throws, saying the condition was not checked. A
409
+ * `false` there would pass a test asserting that something is absent.
410
+ */
411
+ assertVisual(screenshotBase64, condition) {
412
+ return __awaiter(this, void 0, void 0, function* () {
413
+ var _a;
414
+ this.initializeProvider();
415
+ const notChecked = 'so the condition was not checked';
416
+ if (!this.provider) {
417
+ throw new Error(`No AI provider is configured, ${notChecked}. Set XENON_AI_PROVIDER and the provider's key.`);
418
+ }
419
+ const prompt = `
420
+ You are checking a screenshot of a mobile app for an automated test.
421
+
422
+ Condition: ${JSON.stringify(condition)}
423
+
424
+ Is the condition true of what the screenshot shows? Judge only what is visible on the screen.
425
+ Answer with JSON only, exactly: {"result": true or false, "reason": "one sentence on what you see"}
426
+ `.trim();
427
+ let answer;
428
+ try {
429
+ answer = yield this.callProvider(prompt, screenshotBase64);
430
+ }
431
+ catch (err) {
432
+ throw new Error(`The AI provider failed, ${notChecked}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
433
+ }
434
+ if (answer === RATE_LIMITED) {
435
+ throw new Error(`The AI provider is rate-limited, ${notChecked}. Try again later.`);
436
+ }
437
+ const verdict = parseVisualVerdict(answer);
438
+ if (!verdict) {
439
+ const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
440
+ throw new Error(`Xenon could not read the AI provider's answer as true or false, ${notChecked}. ` +
441
+ `It answered: ${JSON.stringify(said)}`);
442
+ }
443
+ return verdict;
444
+ });
445
+ }
446
+ /**
447
+ * A short description of the screen in the screenshot, for
448
+ * `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
449
+ * is no provider or the call fails, so the caller can say why there is none.
450
+ */
451
+ describeScreen(screenshotBase64) {
452
+ return __awaiter(this, void 0, void 0, function* () {
453
+ this.initializeProvider();
454
+ if (!this.provider)
455
+ throw new Error('No AI provider is configured');
456
+ const prompt = `
457
+ You are looking at a screenshot of a mobile app, for a software tester.
458
+ Describe the screen in a few short sentences: which screen it is, its main elements and their state (for example filled in, empty, selected or disabled), and anything unexpected, such as an error message, a system dialog, a crash or a loading indicator.
459
+ Describe only what is visible. Answer in plain text.
460
+ `.trim();
461
+ const answer = yield this.callProvider(prompt, screenshotBase64);
462
+ if (answer === RATE_LIMITED)
463
+ throw new Error('The AI provider is rate-limited');
464
+ return answer;
465
+ });
466
+ }
368
467
  /**
369
468
  * Heals a broken locator using deep LLM reasoning (Tier 5)
370
469
  */
@@ -25,6 +25,7 @@ const logger_1 = __importDefault(require("../logger"));
25
25
  const fs_1 = __importDefault(require("fs"));
26
26
  const path_1 = __importDefault(require("path"));
27
27
  const config_1 = require("../config");
28
+ const labSettings_1 = require("./settings/labSettings");
28
29
  const recordingCleanup_1 = require("./recording/recordingCleanup");
29
30
  /** Subdirectory holding composite (multi-device) outputs, keyed by group id. */
30
31
  const GROUPS_DIR = '_groups';
@@ -34,6 +35,8 @@ let CleanupService = class CleanupService {
34
35
  }
35
36
  /**
36
37
  * Orchestrates build + session + recording cleanup based on retention policy.
38
+ * The build retention window, the build cap and the asset purge are the ones
39
+ * saved on the dashboard's Maintenance page, else `pluginArgs`'.
37
40
  *
38
41
  * Phases:
39
42
  * 1. Purge old builds (by age AND count cap) together with their sessions.
@@ -44,7 +47,9 @@ let CleanupService = class CleanupService {
44
47
  */
45
48
  runCleanup(pluginArgs) {
46
49
  return __awaiter(this, void 0, void 0, function* () {
47
- const { buildCleanupDays = 30, buildCleanupMaxCount = 100, deleteBuildAssets = true, } = pluginArgs;
50
+ // What the Maintenance page saved wins over the startup options, and is
51
+ // read here, at each run, so a change needs no restart. See labSettings.ts.
52
+ const { buildCleanupDays, buildCleanupMaxCount, deleteBuildAssets } = yield (0, labSettings_1.loadEffectiveSettings)(pluginArgs);
48
53
  this.log.info(`Starting cleanup: Retention = ${buildCleanupDays} days, Max Builds = ${buildCleanupMaxCount}, Purge Assets = ${deleteBuildAssets}`);
49
54
  const expirationDate = new Date();
50
55
  expirationDate.setDate(expirationDate.getDate() - buildCleanupDays);