@xenon-device-management/xenon 2.15.0 → 2.16.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 (183) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
  37. package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
  39. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
  59. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  68. package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
  69. package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/openapi/sessions.yaml +7 -5
  78. package/lib/src/app/routers/auth.js +2 -1
  79. package/lib/src/app/routers/users.js +2 -1
  80. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  81. package/lib/src/dashboard/commandLogFields.js +42 -0
  82. package/lib/src/dashboard/event-manager.js +26 -154
  83. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  84. package/lib/src/dashboard/services/failureCategories.js +264 -0
  85. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  86. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  87. package/lib/src/device-utils.js +14 -6
  88. package/lib/src/gateway/forwardToNode.js +4 -11
  89. package/lib/src/gateway/hubSessionToken.js +27 -4
  90. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  91. package/lib/src/helpers/outboundProxy.js +219 -0
  92. package/lib/src/index.js +21 -0
  93. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  94. package/lib/src/interceptors/xenonScripts.js +28 -7
  95. package/lib/src/logger.js +3 -1
  96. package/lib/src/plugin.js +8 -3
  97. package/lib/src/scripts/database-check.js +3 -3
  98. package/lib/src/scripts/initialize-database.js +35 -33
  99. package/lib/src/scripts/run-migrations.js +1079 -27
  100. package/lib/src/services/ApiKeyService.js +16 -5
  101. package/lib/src/services/InterceptorService.js +6 -2
  102. package/lib/src/services/OrphanSweeper.js +6 -0
  103. package/lib/src/services/SelectorStateService.js +11 -7
  104. package/lib/src/services/SelectorVerificationJob.js +6 -2
  105. package/lib/src/services/SessionLifecycleService.js +32 -18
  106. package/lib/src/services/SocketClient.js +11 -0
  107. package/lib/src/services/SocketServer.js +361 -42
  108. package/lib/src/services/TeamService.js +5 -1
  109. package/lib/src/services/TracingService.js +171 -60
  110. package/lib/src/services/UserService.js +14 -3
  111. package/lib/src/services/UserSessionService.js +16 -4
  112. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  113. package/lib/src/services/identity/identityChanges.js +41 -0
  114. package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
  115. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  116. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  117. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  118. package/lib/src/services/token/JwtKeyService.js +8 -2
  119. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  120. package/lib/src/sessions/LocalSession.js +29 -1
  121. package/lib/src/sessions/RemoteSession.js +12 -2
  122. package/lib/src/sessions/SessionManager.js +5 -8
  123. package/lib/test/helpers/driver-log.js +38 -0
  124. package/lib/test/helpers/fake-http-proxy.js +106 -0
  125. package/lib/test/helpers/otlp-probe.js +107 -0
  126. package/lib/test/integration/androidDevices.spec.js +1 -1
  127. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  128. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  129. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  130. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  131. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  132. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  133. package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
  134. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  135. package/lib/test/unit/database-check.spec.js +1 -0
  136. package/lib/test/unit/device-utils.spec.js +13 -4
  137. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  138. package/lib/test/unit/failure-analysis.spec.js +22 -8
  139. package/lib/test/unit/failure-categories.spec.js +24 -22
  140. package/lib/test/unit/failure-categorization.spec.js +393 -0
  141. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  142. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  143. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  144. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  145. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  146. package/lib/test/unit/logcat-ws.spec.js +78 -37
  147. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  148. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  149. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  150. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  151. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  152. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  153. package/lib/test/unit/proxy-option.spec.js +358 -0
  154. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  155. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  156. package/lib/test/unit/run-migrations.test.js +864 -0
  157. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  158. package/lib/test/unit/selector-state-service.spec.js +72 -23
  159. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  160. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  161. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  162. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  163. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  164. package/lib/test/unit/session-device-logs.spec.js +11 -2
  165. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  166. package/lib/test/unit/session-log-indexes.spec.js +1 -1
  167. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  168. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  169. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  170. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  171. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  172. package/lib/tsconfig.tsbuildinfo +1 -1
  173. package/package.json +2 -1
  174. package/schema.json +11 -7
  175. package/scripts/dev/readme-screenshots.js +276 -23
  176. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  177. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  178. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  179. package/scripts/export-openapi.js +32 -0
  180. package/scripts/lib/openapi-for-site.js +79 -0
  181. package/lib/public/assets/index-BOxe43p1.js +0 -219
  182. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  183. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  return (mod && mod.__esModule) ? mod : { "default": mod };
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
- exports.SESSION_MANAGER = exports.SessionManager = exports.HUB_RESTART_CATEGORY = void 0;
54
+ exports.SESSION_MANAGER = exports.SessionManager = void 0;
55
55
  const os_1 = __importDefault(require("os"));
56
56
  const typedi_1 = require("typedi");
57
57
  const prisma_1 = require("../prisma");
@@ -62,12 +62,9 @@ const device_store_1 = require("../data-service/device-store");
62
62
  const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
63
63
  const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
64
64
  const nodeMetrics_1 = require("../services/metrics/nodeMetrics");
65
- /**
66
- * The `failure_category` of a session a restart ended: one still running at
67
- * boot that couldn't be picked up again. The failure analysis writes every
68
- * other category (`ANALYSIS_CATEGORIES`).
69
- */
70
- exports.HUB_RESTART_CATEGORY = 'HUB_RESTART';
65
+ // A session still running at boot that can't be picked up again is filed
66
+ // under it; the failure analysis also writes it for a shutdown's drain.
67
+ const failureCategories_1 = require("../dashboard/services/failureCategories");
71
68
  /**
72
69
  * SessionManager with persistence and recovery capabilities.
73
70
  *
@@ -312,7 +309,7 @@ let SessionManager = class SessionManager {
312
309
  status: 'failed',
313
310
  endTime: new Date(),
314
311
  failure_reason: reason,
315
- failure_category: exports.HUB_RESTART_CATEGORY,
312
+ failure_category: failureCategories_1.HUB_RESTART_CATEGORY,
316
313
  },
317
314
  });
318
315
  }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TestDriverLog = void 0;
13
+ const line_consuming_log_1 = require("appium-xcuitest-driver/build/lib/device/log/line-consuming-log");
14
+ const quietLog = { debug() { }, info() { }, warn() { }, error() { } };
15
+ /**
16
+ * The XCUITest driver's own log class, as an iPhone's (`IOSDeviceLog`) and a
17
+ * simulator's (`IOSSimulatorLog`) `logs.syslog` extend it, with `line()` in
18
+ * place of a device: what Xenon reads is what the driver keeps.
19
+ */
20
+ class TestDriverLog extends line_consuming_log_1.LineConsumingLog {
21
+ constructor() {
22
+ super({ log: quietLog });
23
+ }
24
+ startCapture() {
25
+ return __awaiter(this, void 0, void 0, function* () { });
26
+ }
27
+ stopCapture() {
28
+ return __awaiter(this, void 0, void 0, function* () { });
29
+ }
30
+ get isCapturing() {
31
+ return true;
32
+ }
33
+ /** A line from the device, as the driver's capture hands it on. */
34
+ line(message) {
35
+ this.broadcast(message);
36
+ }
37
+ }
38
+ exports.TestDriverLog = TestDriverLog;
@@ -0,0 +1,106 @@
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.startFakeProxy = startFakeProxy;
16
+ const http_1 = __importDefault(require("http"));
17
+ const net_1 = __importDefault(require("net"));
18
+ /**
19
+ * An HTTP proxy on 127.0.0.1 that forwards what it is sent and notes it: a
20
+ * request in absolute form (http-proxy-agent, axios) is passed on to its
21
+ * target, a CONNECT (https-proxy-agent, so WebSockets) becomes a tunnel, or
22
+ * is refused with `refuseConnect`. A spec can then tell a call that went
23
+ * through the proxy from one that went straight to the target.
24
+ */
25
+ function startFakeProxy() {
26
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
27
+ const seen = [];
28
+ const authorizations = [];
29
+ const sockets = new Set();
30
+ const resolve = (host) => { var _a, _b; return (_b = (_a = opts.hosts) === null || _a === void 0 ? void 0 : _a[host]) !== null && _b !== void 0 ? _b : host; };
31
+ const server = http_1.default.createServer((req, res) => {
32
+ var _a, _b;
33
+ seen.push(`${req.method} ${req.url}`);
34
+ authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
35
+ let target;
36
+ try {
37
+ target = new URL((_b = req.url) !== null && _b !== void 0 ? _b : '');
38
+ }
39
+ catch (_c) {
40
+ res.writeHead(400).end();
41
+ return;
42
+ }
43
+ const upstream = http_1.default.request(Object.assign(Object.assign({}, urlParts(target)), { hostname: resolve(target.hostname), method: req.method, headers: req.headers }), (answer) => {
44
+ var _a;
45
+ res.writeHead((_a = answer.statusCode) !== null && _a !== void 0 ? _a : 502, answer.headers);
46
+ answer.pipe(res);
47
+ });
48
+ upstream.on('error', () => res.writeHead(502).end());
49
+ req.pipe(upstream);
50
+ });
51
+ server.on('connect', (req, client, head) => {
52
+ var _a;
53
+ seen.push(`CONNECT ${req.url}`);
54
+ authorizations.push(String((_a = req.headers['proxy-authorization']) !== null && _a !== void 0 ? _a : ''));
55
+ if (opts.ignoreConnect) {
56
+ sockets.add(client);
57
+ client.on('error', () => client.destroy());
58
+ return;
59
+ }
60
+ if (opts.refuseConnect) {
61
+ client.end('HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n');
62
+ return;
63
+ }
64
+ const [host, port] = String(req.url).split(':');
65
+ const upstream = net_1.default.connect(Number(port), resolve(host), () => {
66
+ client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
67
+ if (head.length)
68
+ upstream.write(head);
69
+ upstream.pipe(client);
70
+ client.pipe(upstream);
71
+ });
72
+ sockets.add(upstream).add(client);
73
+ const drop = () => {
74
+ upstream.destroy();
75
+ client.destroy();
76
+ };
77
+ upstream.on('error', drop);
78
+ client.on('error', drop);
79
+ });
80
+ yield new Promise((resolve, reject) => {
81
+ server.once('error', reject);
82
+ server.listen(0, '127.0.0.1', () => resolve());
83
+ });
84
+ const { port } = server.address();
85
+ return {
86
+ url: `http://127.0.0.1:${port}`,
87
+ seen,
88
+ authorizations,
89
+ close: () => new Promise((resolve) => {
90
+ var _a;
91
+ for (const socket of sockets)
92
+ socket.destroy();
93
+ (_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
94
+ server.close(() => resolve());
95
+ }),
96
+ };
97
+ });
98
+ }
99
+ function urlParts(url) {
100
+ return {
101
+ protocol: url.protocol,
102
+ hostname: url.hostname,
103
+ port: url.port,
104
+ path: `${url.pathname}${url.search}`,
105
+ };
106
+ }
@@ -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.startOtlpProbe = startOtlpProbe;
16
+ exports.attributeOf = attributeOf;
17
+ exports.resetOtelGlobals = resetOtelGlobals;
18
+ exports.flushOtelGlobals = flushOtelGlobals;
19
+ const http_1 = __importDefault(require("http"));
20
+ const api_1 = require("@opentelemetry/api");
21
+ const api_logs_1 = require("@opentelemetry/api-logs");
22
+ /**
23
+ * A stand-in OTLP/HTTP collector on 127.0.0.1. It answers every request 200
24
+ * and keeps what it was sent, so a spec can see where Xenon's telemetry went
25
+ * and in which encoding (Xenon's own exporters send OTLP/JSON; the ones the
26
+ * OTel SDK builds from OTEL_* variables send protobuf).
27
+ */
28
+ function startOtlpProbe() {
29
+ return __awaiter(this, void 0, void 0, function* () {
30
+ const requests = [];
31
+ const server = http_1.default.createServer((req, res) => {
32
+ const chunks = [];
33
+ req.on('data', (chunk) => chunks.push(chunk));
34
+ req.on('end', () => {
35
+ var _a, _b;
36
+ const body = Buffer.concat(chunks);
37
+ const contentType = String((_a = req.headers['content-type']) !== null && _a !== void 0 ? _a : '');
38
+ let json;
39
+ if (contentType.includes('json')) {
40
+ try {
41
+ json = JSON.parse(body.toString('utf8'));
42
+ }
43
+ catch (_c) {
44
+ json = undefined;
45
+ }
46
+ }
47
+ requests.push({ path: (_b = req.url) !== null && _b !== void 0 ? _b : '', contentType, body, json });
48
+ if (contentType.includes('json')) {
49
+ res.writeHead(200, { 'content-type': 'application/json' });
50
+ res.end('{}');
51
+ }
52
+ else {
53
+ res.writeHead(200, { 'content-type': 'application/x-protobuf' });
54
+ res.end();
55
+ }
56
+ });
57
+ });
58
+ yield new Promise((resolve, reject) => {
59
+ server.once('error', reject);
60
+ server.listen(0, '127.0.0.1', () => resolve());
61
+ });
62
+ const { port } = server.address();
63
+ return {
64
+ url: (path) => `http://127.0.0.1:${port}${path}`,
65
+ requests,
66
+ spans: () => requests.flatMap((r) => {
67
+ var _a, _b;
68
+ return ((_b = (_a = r.json) === null || _a === void 0 ? void 0 : _a.resourceSpans) !== null && _b !== void 0 ? _b : []).flatMap((rs) => { var _a; return ((_a = rs.scopeSpans) !== null && _a !== void 0 ? _a : []).flatMap((ss) => { var _a; return ((_a = ss.spans) !== null && _a !== void 0 ? _a : []); }); });
69
+ }),
70
+ close: () => new Promise((resolve) => {
71
+ var _a;
72
+ (_a = server.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(server);
73
+ server.close(() => resolve());
74
+ }),
75
+ };
76
+ });
77
+ }
78
+ /** A span attribute's value, whatever its OTLP/JSON type. */
79
+ function attributeOf(attributes, key) {
80
+ const found = attributes === null || attributes === void 0 ? void 0 : attributes.find((a) => a.key === key);
81
+ if (!found)
82
+ return undefined;
83
+ return Object.values(found.value)[0];
84
+ }
85
+ /**
86
+ * Takes every OTel global (tracer, meter and logger providers, context
87
+ * manager, propagator) off the API, so the next registration wins. The API
88
+ * keeps the first registration for the life of the process and refuses the
89
+ * rest: a spec that leaves one behind decides what every later spec exports.
90
+ */
91
+ function resetOtelGlobals() {
92
+ api_1.trace.disable();
93
+ api_1.metrics.disable();
94
+ api_logs_1.logs.disable();
95
+ api_1.context.disable();
96
+ api_1.propagation.disable();
97
+ }
98
+ /** Exports whatever the registered meter and logger providers hold now. */
99
+ function flushOtelGlobals() {
100
+ return __awaiter(this, void 0, void 0, function* () {
101
+ var _a, _b;
102
+ const meterProvider = api_1.metrics.getMeterProvider();
103
+ yield ((_a = meterProvider.forceFlush) === null || _a === void 0 ? void 0 : _a.call(meterProvider));
104
+ const loggerProvider = api_logs_1.logs.getLoggerProvider();
105
+ yield ((_b = loggerProvider.forceFlush) === null || _b === void 0 ? void 0 : _b.call(loggerProvider));
106
+ });
107
+ }
@@ -133,6 +133,6 @@ describe('Android Test', () => {
133
133
  };
134
134
  yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 1000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
135
135
  .to.be.an('error')
136
- .with.property('message', 'Device is busy or blocked.. Device request: {"platform":"android"}'));
136
+ .with.property('message', 'Device is busy or blocked. Device request: {"platform":"android"}'));
137
137
  }));
138
138
  });
@@ -145,7 +145,7 @@ describe('Max sessions CLI argument test', () => {
145
145
  };
146
146
  yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 6000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
147
147
  .to.be.an('error')
148
- .with.property('message', 'Device is busy or blocked.. Device request: {"platform":"ios","name":"iPhone","deviceType":"simulator"}'));
148
+ .with.property('message', 'Device is busy or blocked. Device request: {"platform":"ios","name":"iPhone","deviceType":"simulator"}'));
149
149
  }));
150
150
  });
151
151
  describe('IOS Simulator Test', () => {
@@ -53,7 +53,7 @@ describe('IOS Test', () => {
53
53
  };
54
54
  yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 6000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
55
55
  .to.be.an('error')
56
- .with.property('message', 'No device matching request.. Device request: {"platform":"ios","name":"iPhone","deviceType":"real"}'));
56
+ .with.property('message', 'No device matching request. Device request: {"platform":"ios","name":"iPhone","deviceType":"real"}'));
57
57
  }));
58
58
  it('Should throw error if the IPA does not match with device type real', () => __awaiter(void 0, void 0, void 0, function* () {
59
59
  yield (0, device_utils_1.initializeStorage)();
@@ -105,7 +105,7 @@ describe('Selector state full flow (integration)', function () {
105
105
  stdio: 'pipe',
106
106
  });
107
107
  prisma = new client_1.PrismaClient({ datasources: { db: { url } } });
108
- socketStub = { emitToDashboard: sinon_1.default.stub() };
108
+ socketStub = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
109
109
  stateService = new SelectorStateService_1.SelectorStateService(prisma, socketStub);
110
110
  verifier = new SelectorVerificationJob_1.SelectorVerificationJob(prisma, socketStub);
111
111
  }));
@@ -116,7 +116,7 @@ describe('Selector state full flow (integration)', function () {
116
116
  fs.unlinkSync(dbPath);
117
117
  }));
118
118
  beforeEach(() => __awaiter(this, void 0, void 0, function* () {
119
- socketStub.emitToDashboard.resetHistory();
119
+ socketStub.emitToDashboardForSelector.resetHistory();
120
120
  // SessionLog → Session → Build — delete children before parents.
121
121
  yield prisma.sessionLog.deleteMany({});
122
122
  yield prisma.session.deleteMany({});
@@ -156,7 +156,7 @@ describe('Selector state full flow (integration)', function () {
156
156
  });
157
157
  (0, chai_1.expect)(fixed.status).to.equal('pending');
158
158
  (0, chai_1.expect)(fixed.fixed_at).to.be.instanceOf(Date);
159
- (0, chai_1.expect)(socketStub.emitToDashboard.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
159
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_FIXED);
160
160
  // Three distinct CI builds, each with one un-healed findElement on the tuple.
161
161
  for (const buildId of ['b-1', 'b-2', 'b-3']) {
162
162
  const sid = `s-${buildId}`;
@@ -190,7 +190,7 @@ describe('Selector state full flow (integration)', function () {
190
190
  (0, chai_1.expect)(final.status).to.equal('resolved');
191
191
  (0, chai_1.expect)(final.clean_builds_count).to.equal(3);
192
192
  (0, chai_1.expect)(final.resolved_at).to.be.instanceOf(Date);
193
- const resolvedEmits = socketStub.emitToDashboard
193
+ const resolvedEmits = socketStub.emitToDashboardForSelector
194
194
  .getCalls()
195
195
  .filter((c) => c.args[0] === SocketEvents_1.SocketEvents.SELECTOR_RESOLVED);
196
196
  (0, chai_1.expect)(resolvedEmits.length).to.equal(1);
@@ -226,6 +226,6 @@ describe('Selector state full flow (integration)', function () {
226
226
  (0, chai_1.expect)(after.regression_count).to.equal(1);
227
227
  (0, chai_1.expect)(after.resolved_at).to.be.null;
228
228
  (0, chai_1.expect)(after.fixed_at).to.be.null;
229
- (0, chai_1.expect)(socketStub.emitToDashboard.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
229
+ (0, chai_1.expect)(socketStub.emitToDashboardForSelector.lastCall.args[0]).to.equal(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED);
230
230
  }));
231
231
  });
@@ -60,3 +60,26 @@ describe('XenonCapabilityManager.getXenonCapabilities — interceptor activation
60
60
  expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(true);
61
61
  });
62
62
  });
63
+ /**
64
+ * `xe:save_device_logs` is a switch like `xe:record_video`: on by default,
65
+ * on only for `true` or "true" once set. Through 2.15 it was read nowhere.
66
+ */
67
+ describe('XenonCapabilityManager.getXenonCapabilities — xe:save_device_logs', () => {
68
+ const saved = (always) => (0, XenonCapabilityManager_1.getXenonCapabilities)(caps(always))[XenonCapabilityManager_1.XENON_CAPABILITIES.SAVE_DEVICE_LOGS];
69
+ it('is on when the session says nothing', () => {
70
+ expect(saved({})).to.equal(true);
71
+ });
72
+ it('is on for true or "true"', () => {
73
+ expect(saved({ 'xe:save_device_logs': true })).to.equal(true);
74
+ expect(saved({ 'xe:saveDeviceLogs': 'true' })).to.equal(true);
75
+ });
76
+ it('is off for false, and for any other value, as every switch is', () => {
77
+ for (const value of [false, 'false', 'yes', 1]) {
78
+ expect(saved({ 'xe:save_device_logs': value }), String(value)).to.equal(false);
79
+ }
80
+ });
81
+ it('is read from xe:options and its xenon:options alias too', () => {
82
+ expect(saved({ 'xe:options': { save_device_logs: false } })).to.equal(false);
83
+ expect(saved({ 'xenon:options': { saveDeviceLogs: false } })).to.equal(false);
84
+ });
85
+ });
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const adbkit_1 = __importDefault(require("@devicefarmer/adbkit"));
19
+ const appium_adb_1 = require("appium-adb");
20
+ const test_container_1 = require("../helpers/test-container");
21
+ const RETRY_MS = 60000;
22
+ /**
23
+ * When adb couldn't be started, or its device tracking couldn't, the server
24
+ * logged "Could not find ADB" without the reason and never looked again: every
25
+ * later discovery answered no phones until a restart, even once adb worked
26
+ * (an SDK installed, a stuck adb server killed). It now says why, and tries
27
+ * again a minute later.
28
+ */
29
+ describe('Android discovery when adb is unavailable', () => {
30
+ const sandbox = sinon_1.default.createSandbox();
31
+ let manager;
32
+ let createADB;
33
+ let trackDevices;
34
+ let errors;
35
+ let clock;
36
+ let savedNodeEnv;
37
+ const workingAdb = () => ({
38
+ adbRemoteHost: null,
39
+ getConnectedDevices: sandbox.stub().resolves([]),
40
+ });
41
+ beforeEach(() => {
42
+ savedNodeEnv = process.env.NODE_ENV;
43
+ clock = sandbox.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
44
+ createADB = sandbox.stub(appium_adb_1.ADB, 'createADB');
45
+ trackDevices = sandbox.stub().resolves({ on: sandbox.stub() });
46
+ sandbox.stub(adbkit_1.default, 'createClient').returns({ trackDevices });
47
+ manager = (0, test_container_1.createTestAndroidManager)({ platform: 'android' });
48
+ sandbox.stub(manager, 'requireSdkRoot').resolves();
49
+ errors = sandbox.stub(manager.log, 'error');
50
+ });
51
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
52
+ sandbox.restore();
53
+ if (savedNodeEnv === undefined)
54
+ delete process.env.NODE_ENV;
55
+ else
56
+ process.env.NODE_ENV = savedNodeEnv;
57
+ yield (0, test_container_1.resetTestContainer)();
58
+ }));
59
+ const discover = () => manager.getDevices({ androidDeviceType: 'both' }, []);
60
+ it('says why adb could not be started', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ createADB.rejects(new Error('Could not find adb. spawn adb ENOENT'));
62
+ (0, chai_1.expect)(yield discover()).to.deep.equal([]);
63
+ const logged = errors.getCalls().map((c) => String(c.args[0]));
64
+ (0, chai_1.expect)(logged.some((m) => m.includes('spawn adb ENOENT')), logged.join('\n')).to.equal(true);
65
+ }));
66
+ it('tries adb again a minute later, and not before', () => __awaiter(void 0, void 0, void 0, function* () {
67
+ const adb = workingAdb();
68
+ createADB.onFirstCall().rejects(new Error('spawn adb ENOENT'));
69
+ createADB.onSecondCall().resolves(adb);
70
+ yield discover();
71
+ clock.tick(RETRY_MS - 1000);
72
+ yield discover();
73
+ (0, chai_1.expect)(createADB.callCount, 'within the minute').to.equal(1);
74
+ clock.tick(2000);
75
+ yield discover();
76
+ (0, chai_1.expect)(createADB.callCount, 'after the minute').to.equal(2);
77
+ (0, chai_1.expect)(adb.getConnectedDevices.called, 'discovery ran').to.equal(true);
78
+ }));
79
+ it('says why device tracking failed, keeps listing phones, and tries it again', () => __awaiter(void 0, void 0, void 0, function* () {
80
+ process.env.NODE_ENV = 'development';
81
+ const adb = workingAdb();
82
+ createADB.resolves(adb);
83
+ trackDevices.onFirstCall().rejects(new Error('connect ECONNREFUSED 127.0.0.1:5037'));
84
+ yield discover();
85
+ const logged = errors.getCalls().map((c) => String(c.args[0]));
86
+ (0, chai_1.expect)(logged.some((m) => m.includes('ECONNREFUSED')), logged.join('\n')).to.equal(true);
87
+ yield discover();
88
+ (0, chai_1.expect)(adb.getConnectedDevices.callCount, 'phones still listed').to.equal(2);
89
+ (0, chai_1.expect)(trackDevices.callCount, 'within the minute').to.equal(1);
90
+ clock.tick(RETRY_MS + 1000);
91
+ yield discover();
92
+ (0, chai_1.expect)(trackDevices.callCount, 'after the minute').to.equal(2);
93
+ }));
94
+ it('starts one device tracker when two discoveries start adb at once', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ // Each tracker runs the whole plug-in handling again for every phone
96
+ // plugged in, and nothing could end the extra one.
97
+ process.env.NODE_ENV = 'development';
98
+ createADB.resolves(workingAdb());
99
+ const pending = [];
100
+ trackDevices.callsFake(() => new Promise((resolve) => pending.push(() => resolve({ on: sandbox.stub() }))));
101
+ const both = Promise.all([discover(), discover()]);
102
+ while (pending.length === 0)
103
+ yield new Promise((r) => setImmediate(r));
104
+ // Time for a second discovery to reach the tracker too, if it would.
105
+ for (let i = 0; i < 20; i++)
106
+ yield new Promise((r) => setImmediate(r));
107
+ pending.forEach((start) => start());
108
+ yield both;
109
+ (0, chai_1.expect)(createADB.callCount, 'adb started').to.equal(1);
110
+ (0, chai_1.expect)(trackDevices.callCount, 'trackers started').to.equal(1);
111
+ }));
112
+ });