@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
@@ -0,0 +1,107 @@
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
+ exports.isUnsetProxy = isUnsetProxy;
16
+ exports.parseProxySetting = parseProxySetting;
17
+ exports.localIPv4Addresses = localIPv4Addresses;
18
+ exports.pointsAtThisMachine = pointsAtThisMachine;
19
+ exports.isPortListening = isPortListening;
20
+ exports.isDeadCaptureProxy = isDeadCaptureProxy;
21
+ const net_1 = __importDefault(require("net"));
22
+ const os_1 = __importDefault(require("os"));
23
+ /**
24
+ * Reading a phone's global HTTP proxy (`settings get global http_proxy`), and
25
+ * telling one Xenon's network capture left behind from one somebody set on
26
+ * purpose.
27
+ */
28
+ /** Android's ways of saying "no proxy": unset (`null`), empty, or `:0`. */
29
+ function isUnsetProxy(value) {
30
+ const v = (value !== null && value !== void 0 ? value : '').trim();
31
+ return v === '' || v === 'null' || v === ':0';
32
+ }
33
+ /** `host:port`, or null for no proxy or anything else. */
34
+ function parseProxySetting(value) {
35
+ if (isUnsetProxy(value))
36
+ return null;
37
+ const v = value.trim();
38
+ const colon = v.lastIndexOf(':');
39
+ if (colon <= 0)
40
+ return null;
41
+ const host = v.slice(0, colon);
42
+ const portText = v.slice(colon + 1);
43
+ if (!/^\d+$/.test(portText))
44
+ return null;
45
+ const port = Number(portText);
46
+ if (port < 1 || port > 65535)
47
+ return null;
48
+ return { host, port };
49
+ }
50
+ /** This machine's own IPv4 addresses, as a phone on its network reaches it. */
51
+ function localIPv4Addresses() {
52
+ const out = [];
53
+ for (const infos of Object.values(os_1.default.networkInterfaces())) {
54
+ for (const info of infos !== null && infos !== void 0 ? infos : []) {
55
+ if (info.family === 'IPv4' && !info.internal)
56
+ out.push(info.address);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * Whether a phone that uses `host` as its proxy reaches this machine: through
63
+ * `adb reverse` (127.0.0.1, the phone itself), an emulator's alias for its
64
+ * host (10.0.2.2), or this machine's own address (the interceptor's LAN
65
+ * fallback). These are the only hosts the interceptor ever sets.
66
+ */
67
+ function pointsAtThisMachine(host, ownAddresses) {
68
+ const h = host.trim().toLowerCase();
69
+ return h === '127.0.0.1' || h === 'localhost' || h === '10.0.2.2' || ownAddresses.includes(h);
70
+ }
71
+ /**
72
+ * Whether something on this machine accepts a TCP connection on `port`.
73
+ * Anything but a refusal counts as yes (a timeout too): only a port known to be
74
+ * dead may have a proxy that points at it cleared.
75
+ */
76
+ function isPortListening(port, timeoutMs = 1000) {
77
+ return new Promise((resolve) => {
78
+ const socket = net_1.default.connect({ host: '127.0.0.1', port });
79
+ const done = (answer) => {
80
+ socket.removeAllListeners();
81
+ socket.destroy();
82
+ resolve(answer);
83
+ };
84
+ socket.setTimeout(timeoutMs, () => done(true));
85
+ socket.once('connect', () => done(true));
86
+ socket.once('error', (err) => done(err.code !== 'ECONNREFUSED'));
87
+ });
88
+ }
89
+ /**
90
+ * Whether `value` is a proxy Xenon's network capture set and nobody serves any
91
+ * more: it points at this machine, on one of this server's capture ports, and
92
+ * nothing answers there. A phone with such a proxy has no network at all. A
93
+ * proxy that points anywhere else, or at a port that answers (a capture that
94
+ * is running, maybe another Xenon's on this machine), is never one.
95
+ */
96
+ function isDeadCaptureProxy(value_1, captureRange_1) {
97
+ return __awaiter(this, arguments, void 0, function* (value, captureRange, probe = isPortListening) {
98
+ const proxy = parseProxySetting(value);
99
+ if (!proxy)
100
+ return false;
101
+ if (!pointsAtThisMachine(proxy.host, localIPv4Addresses()))
102
+ return false;
103
+ if (proxy.port < captureRange[0] || proxy.port > captureRange[1])
104
+ return false;
105
+ return !(yield probe(proxy.port));
106
+ });
107
+ }
@@ -59,6 +59,8 @@ const sharp_1 = __importDefault(require("sharp"));
59
59
  const AIService_1 = require("../AIService");
60
60
  const logger_1 = __importDefault(require("../../logger"));
61
61
  const crypto_1 = __importDefault(require("crypto"));
62
+ const ocrTextMatch_1 = require("./ocrTextMatch");
63
+ const screenScale_1 = require("./screenScale");
62
64
  // Mobile UIs have sparse text on complex backgrounds - reduce contrast to preserve readability
63
65
  const DEFAULT_CONTRAST = 1.2; // Slightly above 1.0 for mild enhancement (was 0.8 which was too aggressive)
64
66
  const UPSCALE_THRESHOLD = 800; // Only upscale very small images (was 1500)
@@ -379,10 +381,17 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
379
381
  }
380
382
  });
381
383
  }
384
+ /** The OCR words with usable boxes, in the screenshot's pixels. */
385
+ wordBoxes(words) {
386
+ return words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
387
+ }
382
388
  /**
383
- * Proactive OCR Search: Finds elements matching text even if not in XML
384
- */
385
- /**
389
+ * Proactive OCR Search: Finds elements matching text even if not in XML.
390
+ *
391
+ * The text may be several words ("Sign in"); see ocrTextMatch.ts. Each
392
+ * element's rect is in the driver's coordinates (points on iOS), like a
393
+ * real element's, so tapping it lands on the text.
394
+ *
386
395
  * `throwOnError`: fail instead of answering "no match" when the screenshot
387
396
  * or OCR fails. Device control's "Test locator" asks for it; an Appium
388
397
  * findElement doesn't, and turns the empty list into NoSuchElement.
@@ -398,17 +407,15 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
398
407
  this.logger.warn(`OCR proactive search yielded no words. Text content: "${ocrText.trim().substring(0, 100)}..."`);
399
408
  return [];
400
409
  }
401
- const matches = words.filter((w) => w.text && w.text.toLowerCase().includes(text.toLowerCase()) && w.confidence > 60);
410
+ const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), String(text !== null && text !== void 0 ? text : '')).filter((m) => m.confidence > 60);
411
+ if (matches.length === 0)
412
+ return [];
413
+ const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
402
414
  return matches.map((m) => {
403
415
  const el = {
404
416
  id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
405
417
  text: m.text,
406
- rect: {
407
- x: m.bbox.x0,
408
- y: m.bbox.y0,
409
- width: m.bbox.x1 - m.bbox.x0,
410
- height: m.bbox.y1 - m.bbox.y0,
411
- },
418
+ rect: (0, screenScale_1.toDriverRect)({ x: m.x0, y: m.y0, width: m.x1 - m.x0, height: m.y1 - m.y0 }, scale),
412
419
  confidence: m.confidence / 100,
413
420
  };
414
421
  this.virtualElementStore.set(el.id, el);
@@ -433,14 +440,12 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
433
440
  const screenshot = yield driver.getScreenshot();
434
441
  const coordinates = yield AIService_1.AI_SERVICE.visualFind(screenshot, iconDescription, opts);
435
442
  if (coordinates) {
443
+ // The AI answers in the screenshot's pixels; the rect is the driver's
444
+ // (points on iOS), like a real element's.
445
+ const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
436
446
  const el = {
437
447
  id: `omni_ai_${Date.now()}`,
438
- rect: {
439
- x: coordinates.x - 20,
440
- y: coordinates.y - 20,
441
- width: 40,
442
- height: 40,
443
- },
448
+ rect: (0, screenScale_1.toDriverRect)({ x: coordinates.x - 20, y: coordinates.y - 20, width: 40, height: 40 }, scale),
444
449
  confidence: 0.85,
445
450
  };
446
451
  this.virtualElementStore.set(el.id, el);
@@ -460,6 +465,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
460
465
  */
461
466
  analyzeScreen(driver) {
462
467
  return __awaiter(this, void 0, void 0, function* () {
468
+ var _a;
463
469
  this.logger.info('Performing full screen analysis...');
464
470
  try {
465
471
  const screenshot = yield driver.getScreenshot();
@@ -470,32 +476,28 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
470
476
  this.validateBuffer(buffer, 'Screen Analysis');
471
477
  // 1. Get all text via OCR using the robust worker flow
472
478
  const { text: ocrText, words } = yield this.performOcr(buffer);
473
- // 2. Ask AI for qualitative analysis (Non-blocking)
474
- let aiAnalysis = null;
479
+ // 2. Ask the AI provider about the screen, with the screenshot. Its
480
+ // failure leaves the OCR answer standing, and says why there are no
481
+ // insights. Through 2.13.2 this sent the screenshot's base64 as a file
482
+ // path, so the provider got no image and a "why did this test fail"
483
+ // prompt, and answered about no screen at all.
484
+ let aiInsights = null;
485
+ let aiError;
475
486
  try {
476
- aiAnalysis = yield AIService_1.AI_SERVICE.analyzeFailure({
477
- sessionId: driver.sessionId,
478
- failureReason: 'Screen Analysis Request',
479
- commandLogs: [],
480
- deviceLogs: [],
481
- screenshotPath: screenshot, // In a real scenario, we'd save this to a file first or pass base64
482
- });
487
+ aiInsights = yield AIService_1.AI_SERVICE.describeScreen(screenshot);
483
488
  }
484
489
  catch (aiErr) {
485
- this.logger.warn(`AI insights skipped: ${aiErr.message}`);
490
+ aiError = (_a = aiErr === null || aiErr === void 0 ? void 0 : aiErr.message) !== null && _a !== void 0 ? _a : String(aiErr);
491
+ this.logger.warn(`AI insights skipped: ${aiError}`);
486
492
  }
487
- return {
488
- timestamp: new Date().toISOString(),
489
- ocr: {
493
+ return Object.assign({ timestamp: new Date().toISOString(), ocr: {
490
494
  text: ocrText,
491
495
  words: words.map((w) => ({
492
496
  text: w.text || '',
493
497
  confidence: w.confidence || 0,
494
498
  bbox: w.bbox || { x0: 0, y0: 0, x1: 0, y1: 0 },
495
499
  })),
496
- },
497
- ai_insights: aiAnalysis,
498
- };
500
+ }, ai_insights: aiInsights }, (aiError !== undefined ? { ai_insights_error: aiError } : {}));
499
501
  }
500
502
  catch (err) {
501
503
  this.logger.error(`Screen analysis failed: ${err.message}`);
@@ -511,6 +513,11 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
511
513
  }
512
514
  /**
513
515
  * Omni-Click: Find a target by OCR text and click its center using W3C actions.
516
+ *
517
+ * The text may be several words ("Sign in"); see ocrTextMatch.ts. The tap
518
+ * and `target` are in the driver's coordinates (points on iOS). A failed
519
+ * screenshot or OCR, or a screen size that can't be read on iOS, fails;
520
+ * `clicked: false` means the text was looked for and not found.
514
521
  */
515
522
  omniClickByText(driver, req) {
516
523
  return __awaiter(this, void 0, void 0, function* () {
@@ -533,22 +540,21 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
533
540
  const screenshot = yield driver.getScreenshot();
534
541
  const buffer = Buffer.from(screenshot, 'base64');
535
542
  const { words } = yield this.performOcr(buffer);
536
- const normalized = words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
537
- const matches = normalized
538
- .filter((w) => w.text.toLowerCase().includes(text.toLowerCase()))
539
- .sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
543
+ const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), text).sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
540
544
  if (matches.length === 0) {
541
545
  return { clicked: false, message: `No OCR match found for text "${text}"` };
542
546
  }
543
547
  const pick = matches[Math.min(index - 1, matches.length - 1)];
548
+ const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
549
+ const found = (0, screenScale_1.toDriverRect)({ x: pick.x0, y: pick.y0, width: pick.x1 - pick.x0, height: pick.y1 - pick.y0 }, scale);
544
550
  const rect = {
545
- x: Math.max(0, Math.round(pick.x0)),
546
- y: Math.max(0, Math.round(pick.y0)),
547
- width: Math.max(1, Math.round(pick.x1 - pick.x0)),
548
- height: Math.max(1, Math.round(pick.y1 - pick.y0)),
551
+ x: Math.max(0, Math.round(found.x)),
552
+ y: Math.max(0, Math.round(found.y)),
553
+ width: Math.max(1, Math.round(found.width)),
554
+ height: Math.max(1, Math.round(found.height)),
549
555
  };
550
- const cx = Math.round(rect.x + rect.width / 2);
551
- const cy = Math.round(rect.y + rect.height / 2);
556
+ const cx = Math.round(found.x + found.width / 2);
557
+ const cy = Math.round(found.y + found.height / 2);
552
558
  // Click via touch actions
553
559
  yield driver.performActions([
554
560
  {
@@ -583,6 +589,10 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
583
589
  }
584
590
  /**
585
591
  * Omni-Click by Icon: Find a target by visual description and click its center.
592
+ *
593
+ * Fails when it can't look (no AI provider, a failed screenshot or AI call);
594
+ * `clicked: false` means the AI looked and found nothing. Through 2.13.2 every
595
+ * one of those answered `clicked: false`, "No visual match found".
586
596
  */
587
597
  omniClickByIcon(driver, req) {
588
598
  return __awaiter(this, void 0, void 0, function* () {
@@ -596,7 +606,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
596
606
  };
597
607
  }
598
608
  this.logger.info(`Omni-Click: searching for icon "${icon}" via AI...`);
599
- const element = yield this.findByIcon(driver, icon);
609
+ const element = yield this.findByIcon(driver, icon, { throwOnError: true });
600
610
  if (!element) {
601
611
  return { clicked: false, message: `No visual match found for icon "${icon}"` };
602
612
  }
@@ -20,6 +20,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.VisionAssertionService = void 0;
22
22
  const typedi_1 = require("typedi");
23
+ const base_driver_1 = require("@appium/base-driver");
23
24
  const AIService_1 = require("../AIService");
24
25
  const logger_1 = __importDefault(require("../../logger"));
25
26
  let VisionAssertionService = class VisionAssertionService {
@@ -27,38 +28,40 @@ let VisionAssertionService = class VisionAssertionService {
27
28
  this.logger = logger_1.default.scope('VisionAssertion');
28
29
  }
29
30
  /**
30
- * Asserts a visual state using AI reasoning
31
+ * `xenon: assertVisualState`: whether `instruction`, a condition in plain
32
+ * words, holds on the device's screen now, as the AI provider judges it from
33
+ * a screenshot.
34
+ *
35
+ * Answers `{ result, message }` only with the provider's own verdict.
36
+ * Whenever there is none it fails, saying the condition was not checked: no
37
+ * condition, no screenshot, no AI provider, a failed or rate-limited call,
38
+ * or an answer that isn't true or false. It never answers `false` for "could
39
+ * not check": a test asserting that something is absent would pass. Through
40
+ * 2.13.2 it answered `{ result: true, message: 'Assertion placeholder' }` in
41
+ * all of those cases.
31
42
  */
32
43
  assertState(driver, instruction) {
33
44
  return __awaiter(this, void 0, void 0, function* () {
34
- this.logger.info(`Asserting visual state: "${instruction}"`);
45
+ var _a;
46
+ const condition = typeof instruction === 'string' ? instruction.trim() : '';
47
+ if (!condition) {
48
+ throw new base_driver_1.errors.InvalidArgumentError('assertVisualState needs a condition to check, as a string or { instruction }, ' +
49
+ "for example 'The cart is empty'.");
50
+ }
51
+ this.logger.info(`Asserting visual state: "${condition}"`);
52
+ let screenshot;
35
53
  try {
36
- const screenshot = yield driver.getScreenshot();
37
- const prompt = `
38
- You are a visual testing quality agent.
39
- Your task is to verify if the following condition is true based on the provided screenshot.
40
-
41
- Condition: "${instruction}"
42
-
43
- Response Format: JSON only, strictly { "result": boolean, "message": "detailed explanation of what you see" }.
44
- `;
45
- const response = yield AIService_1.AI_SERVICE.visualFind(screenshot, prompt);
46
- // Note: We use visualFind which calls analyze. We expect a JSON response.
47
- // Since analyze returns raw text, we attempt to parse it.
48
- if (response && response.result !== undefined) {
49
- return {
50
- result: response.result,
51
- message: response.message || 'Assertion evaluated successfully.',
52
- };
53
- }
54
- // Fallback: If AI_SERVICE.visualFind was used literally, it might not return what we expect.
55
- // Let's assume a more direct path if needed, but for now we'll stick to this pattern.
56
- return { result: true, message: 'Assertion placeholder' };
54
+ screenshot = yield driver.getScreenshot();
57
55
  }
58
56
  catch (err) {
59
- this.logger.error(`Visual assertion failed: ${err.message}`);
60
- return { result: false, message: `Error: ${err.message}` };
57
+ throw new Error(`No screenshot could be taken, so the condition was not checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
58
+ }
59
+ if (!screenshot) {
60
+ throw new Error('No screenshot could be taken, so the condition was not checked.');
61
61
  }
62
+ const verdict = yield AIService_1.AI_SERVICE.assertVisual(screenshot, condition);
63
+ this.logger.info(`"${condition}": ${verdict.result} (${verdict.message})`);
64
+ return verdict;
62
65
  });
63
66
  }
64
67
  };
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Finding a text, one word or several, among the words OCR read off a
4
+ * screenshot. Tesseract gives one box per word, so a text with a space in it
5
+ * ("Sign in") is looked for across neighbouring words on one line, and its
6
+ * box is the box around the words it covers.
7
+ *
8
+ * Pure: boxes in, boxes out, in the screenshot's pixels.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.wordRuns = wordRuns;
12
+ exports.findText = findText;
13
+ /**
14
+ * Two words further apart than this many times their height are not
15
+ * neighbours: a space is about a third of a word's height, and a gap this
16
+ * wide is another column, such as "Cancel" and "Done" at either end of a bar.
17
+ */
18
+ const MAX_GAP_IN_HEIGHTS = 1.5;
19
+ const normalize = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
20
+ const heightOf = (w) => Math.max(1, w.y1 - w.y0);
21
+ /** Whether a word sits on a line: their heights overlap by at least half the shorter one. */
22
+ function onLine(line, word) {
23
+ const overlap = Math.min(line.y1, word.y1) - Math.max(line.y0, word.y0);
24
+ return overlap > 0 && overlap >= Math.min(heightOf(line), heightOf(word)) / 2;
25
+ }
26
+ /**
27
+ * The words in runs of neighbours: on one line, left to right, with no wide
28
+ * gap between them. Runs are in reading order.
29
+ */
30
+ function wordRuns(words) {
31
+ const lines = [];
32
+ for (const word of [...words].sort((a, b) => a.y0 - b.y0 || a.x0 - b.x0)) {
33
+ const line = lines.find((l) => onLine(l, word));
34
+ if (line) {
35
+ line.words.push(word);
36
+ line.y0 = Math.min(line.y0, word.y0);
37
+ line.y1 = Math.max(line.y1, word.y1);
38
+ }
39
+ else {
40
+ lines.push({ y0: word.y0, y1: word.y1, words: [word] });
41
+ }
42
+ }
43
+ const runs = [];
44
+ for (const line of lines.sort((a, b) => a.y0 - b.y0)) {
45
+ let run = [];
46
+ for (const word of line.words.sort((a, b) => a.x0 - b.x0)) {
47
+ const previous = run[run.length - 1];
48
+ const gap = previous ? word.x0 - previous.x1 : 0;
49
+ if (previous && gap > MAX_GAP_IN_HEIGHTS * Math.max(heightOf(previous), heightOf(word))) {
50
+ runs.push(run);
51
+ run = [];
52
+ }
53
+ run.push(word);
54
+ }
55
+ if (run.length)
56
+ runs.push(run);
57
+ }
58
+ return runs;
59
+ }
60
+ /**
61
+ * Every place `text` appears, ignoring case and runs of spaces, within a word
62
+ * or across neighbouring words: "Sign in" in "Sign" "in", "password" in
63
+ * "password?". Each match covers the words it touches: its box surrounds
64
+ * them, its text is theirs joined with spaces, and its confidence is the
65
+ * lowest of theirs. A one-word text matches inside one word, as it always
66
+ * did. Matches are in reading order.
67
+ */
68
+ function findText(words, text) {
69
+ const wanted = normalize(text);
70
+ if (!wanted)
71
+ return [];
72
+ const matches = [];
73
+ for (const run of wordRuns(words)) {
74
+ // The run as one string, remembering where each word sits in it.
75
+ let line = '';
76
+ const spans = [];
77
+ for (const word of run) {
78
+ if (line)
79
+ line += ' ';
80
+ const start = line.length;
81
+ line += normalize(word.text);
82
+ spans.push({ start, end: line.length });
83
+ }
84
+ const seen = new Set();
85
+ for (let at = line.indexOf(wanted); at !== -1; at = line.indexOf(wanted, at + 1)) {
86
+ const end = at + wanted.length;
87
+ const first = spans.findIndex((s) => s.end > at);
88
+ const last = spans.findIndex((s) => s.end >= end);
89
+ const key = `${first}-${last}`;
90
+ if (first === -1 || last === -1 || seen.has(key))
91
+ continue;
92
+ seen.add(key);
93
+ const covered = run.slice(first, last + 1);
94
+ matches.push({
95
+ x0: Math.min(...covered.map((w) => w.x0)),
96
+ y0: Math.min(...covered.map((w) => w.y0)),
97
+ x1: Math.max(...covered.map((w) => w.x1)),
98
+ y1: Math.max(...covered.map((w) => w.y1)),
99
+ text: covered.map((w) => w.text).join(' '),
100
+ confidence: Math.min(...covered.map((w) => w.confidence)),
101
+ });
102
+ }
103
+ }
104
+ return matches;
105
+ }
@@ -0,0 +1,68 @@
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
+ exports.SAME_SCALE = void 0;
16
+ exports.driverUsesPoints = driverUsesPoints;
17
+ exports.screenScaleOf = screenScaleOf;
18
+ exports.toDriverRect = toDriverRect;
19
+ const sharp_1 = __importDefault(require("sharp"));
20
+ exports.SAME_SCALE = { x: 1, y: 1 };
21
+ /**
22
+ * Whether the driver's coordinates are points rather than the screenshot's
23
+ * pixels. A driver Xenon can't place (device control's screenshot-only
24
+ * stand-in, which never taps) keeps pixels.
25
+ */
26
+ function driverUsesPoints(driver) {
27
+ var _a, _b, _c, _d;
28
+ const platform = String((_d = (_b = (_a = driver === null || driver === void 0 ? void 0 : driver.caps) === null || _a === void 0 ? void 0 : _a.platformName) !== null && _b !== void 0 ? _b : (_c = driver === null || driver === void 0 ? void 0 : driver.opts) === null || _c === void 0 ? void 0 : _c.platformName) !== null && _d !== void 0 ? _d : '');
29
+ return ['ios', 'tvos'].includes(platform.toLowerCase());
30
+ }
31
+ /**
32
+ * The scale between `screenshotBase64` and the driver's coordinates:
33
+ * 1 unless the driver uses points, then the screenshot's size over the
34
+ * window's. Throws when that can't be worked out on a driver that uses
35
+ * points, since a tap in pixels there lands in the wrong place.
36
+ */
37
+ function screenScaleOf(driver, screenshotBase64) {
38
+ return __awaiter(this, void 0, void 0, function* () {
39
+ if (!driverUsesPoints(driver))
40
+ return exports.SAME_SCALE;
41
+ const image = yield (0, sharp_1.default)(Buffer.from(screenshotBase64, 'base64'))
42
+ .metadata()
43
+ .catch((err) => {
44
+ throw new Error(`The screenshot's size could not be read: ${err.message}`);
45
+ });
46
+ const window = typeof driver.getWindowRect === 'function'
47
+ ? yield driver.getWindowRect()
48
+ : yield driver.getWindowSize();
49
+ const x = Number(image.width) / Number(window === null || window === void 0 ? void 0 : window.width);
50
+ const y = Number(image.height) / Number(window === null || window === void 0 ? void 0 : window.height);
51
+ if (!(Number.isFinite(x) && x > 0 && Number.isFinite(y) && y > 0)) {
52
+ throw new Error(`The screen size could not be worked out (screenshot ${image.width}x${image.height}, ` +
53
+ `window ${window === null || window === void 0 ? void 0 : window.width}x${window === null || window === void 0 ? void 0 : window.height}).`);
54
+ }
55
+ return { x, y };
56
+ });
57
+ }
58
+ /** A rect found in the screenshot, in the driver's coordinates. */
59
+ function toDriverRect(rect, scale) {
60
+ if (scale.x === 1 && scale.y === 1)
61
+ return rect;
62
+ return {
63
+ x: rect.x / scale.x,
64
+ y: rect.y / scale.y,
65
+ width: rect.width / scale.x,
66
+ height: rect.height / scale.y,
67
+ };
68
+ }
@@ -18,10 +18,19 @@ const config_1 = require("../../config");
18
18
  * and are NOT counted against this gate — they bypass it entirely.
19
19
  */
20
20
  let ConcurrencyGate = class ConcurrencyGate {
21
- constructor(limit) {
22
- var _a;
21
+ /**
22
+ * An explicit `limit` is fixed. Without one the gate follows
23
+ * `config.maxConcurrentRecordings` as it stands at each admission, not as it
24
+ * stood when the gate was built: the gate is created on first use, and the
25
+ * `maxConcurrentRecordings` option is applied at boot.
26
+ */
27
+ constructor(fixedLimit) {
28
+ this.fixedLimit = fixedLimit;
23
29
  this.active = new Set();
24
- this.limit = (_a = limit !== null && limit !== void 0 ? limit : config_1.config.maxConcurrentRecordings) !== null && _a !== void 0 ? _a : 4;
30
+ }
31
+ get limit() {
32
+ var _a, _b;
33
+ return (_b = (_a = this.fixedLimit) !== null && _a !== void 0 ? _a : config_1.config.maxConcurrentRecordings) !== null && _b !== void 0 ? _b : 4;
25
34
  }
26
35
  /**
27
36
  * Atomic admission: either ALL recordingIds are admitted, or none are.
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.SelfHealingSwitch = void 0;
22
+ const typedi_1 = require("typedi");
23
+ const logger_1 = __importDefault(require("../../logger"));
24
+ const web_config_service_1 = require("../../data-service/web-config-service");
25
+ const labSettings_1 = require("./labSettings");
26
+ /**
27
+ * Whether self-healing is on, for the command interceptor, which asks at every
28
+ * command and so must never read the database.
29
+ *
30
+ * The value saved on the Settings page (the `WebConfig` row `enableSelfHealing`)
31
+ * is kept here, loaded once at boot and replaced when `POST /config` saves it.
32
+ * The rule is the one every lab setting follows (`selfHealingEnabled`): the
33
+ * saved value, else the plugin option the server started with, else on. The
34
+ * interceptor passes the options it was given, so the startup half is always
35
+ * the live one.
36
+ *
37
+ * Through 2.13 the toggle was stored nowhere and the interceptor read the
38
+ * startup options alone, so switching healing off in the dashboard did nothing.
39
+ */
40
+ let SelfHealingSwitch = class SelfHealingSwitch {
41
+ /** Whether a failed findElement goes on to the healing tiers, now. */
42
+ isEnabled(startup) {
43
+ return (0, labSettings_1.selfHealingEnabled)(startup.enableSelfHealing, this.saved);
44
+ }
45
+ /**
46
+ * Reads the saved value, once, at boot. A database that can't be read leaves
47
+ * the startup option in charge, as it does for the cleanup job, and is said
48
+ * once here rather than at every command.
49
+ */
50
+ load() {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ try {
53
+ const { enableSelfHealing } = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
54
+ this.saved = typeof enableSelfHealing === 'boolean' ? enableSelfHealing : undefined;
55
+ }
56
+ catch (err) {
57
+ this.saved = undefined;
58
+ logger_1.default.warn(`Could not read the saved self-healing setting, using the startup option: ${err === null || err === void 0 ? void 0 : err.message}`);
59
+ }
60
+ });
61
+ }
62
+ /** What `POST /config` just saved, in force for the next command. */
63
+ set(saved) {
64
+ this.saved = saved;
65
+ }
66
+ };
67
+ exports.SelfHealingSwitch = SelfHealingSwitch;
68
+ exports.SelfHealingSwitch = SelfHealingSwitch = __decorate([
69
+ (0, typedi_1.Service)()
70
+ ], SelfHealingSwitch);