@xenon-device-management/xenon 2.15.0 → 2.17.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 (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.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/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -0,0 +1,180 @@
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 supertest_1 = __importDefault(require("supertest"));
18
+ const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
19
+ const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
20
+ const scratch_database_1 = require("../helpers/scratch-database");
21
+ const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
22
+ // The driver's own log class (test/helpers/driver-log.ts): an iPhone's and a
23
+ // simulator's `logs.syslog` extend it, so what Xenon reads is what the driver keeps.
24
+ const driver_log_1 = require("../helpers/driver-log");
25
+ // How the Device logs tab reads an iPhone line's level (web log-derive.ts).
26
+ const DASHBOARD_SYSLOG = /<(Error|Fault|Warning)>/;
27
+ class TestDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
28
+ context() {
29
+ return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4723, nodeId: 'this-server' };
30
+ }
31
+ flushIntervalMs() {
32
+ return 60000;
33
+ }
34
+ }
35
+ const IPHONE = {
36
+ udid: '00008110-000A',
37
+ platform: 'ios',
38
+ realDevice: true,
39
+ host: 'http://127.0.0.1:4723',
40
+ nodeId: 'this-server',
41
+ };
42
+ const SIMULATOR = Object.assign(Object.assign({}, IPHONE), { udid: 'SIM-1', realDevice: false });
43
+ const syslog = (n, level = 'Notice') => `Oct 5 20:31:${String(n % 60).padStart(2, '0')} iPhone Shop[4127] <${level}>: line ${n}`;
44
+ function deviceLogs(sessionId) {
45
+ return __awaiter(this, void 0, void 0, function* () {
46
+ const res = yield (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(selector_health_fixture_1.ADMIN)).get(`/session/${sessionId}/logs/device`);
47
+ (0, chai_1.expect)(res.status, res.text).to.equal(200);
48
+ return res.body;
49
+ });
50
+ }
51
+ describe("An iPhone or simulator session's Device logs", function () {
52
+ this.timeout(60000);
53
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
54
+ let logs;
55
+ let sessionId;
56
+ let driverLog;
57
+ let count = 0;
58
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
59
+ count += 1;
60
+ sessionId = `dl-ios-${count}`;
61
+ yield scratch.db.session.create({
62
+ data: {
63
+ id: sessionId,
64
+ device_udid: IPHONE.udid,
65
+ device_platform: 'ios',
66
+ device_version: '26.0',
67
+ desired_capabilities: '{}',
68
+ session_capabilities: '{}',
69
+ node_id: 'this-server',
70
+ has_live_video: false,
71
+ },
72
+ });
73
+ logs = new TestDeviceLogs();
74
+ driverLog = new driver_log_1.TestDriverLog();
75
+ }));
76
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
77
+ yield logs.stop(sessionId);
78
+ }));
79
+ it('keeps the lines from the create and from the run, each once, in order', () => __awaiter(this, void 0, void 0, function* () {
80
+ // Logged during the create, before Xenon could listen: in the driver's buffer.
81
+ for (let i = 0; i < 3; i++)
82
+ driverLog.line(syslog(i));
83
+ yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
84
+ for (let i = 3; i < 250; i++)
85
+ driverLog.line(syslog(i));
86
+ yield logs.stop(sessionId);
87
+ (0, chai_1.expect)(driverLog.listenerCount('output')).to.equal(0);
88
+ driverLog.line(syslog(999)); // after the session: not its line
89
+ const rows = yield deviceLogs(sessionId);
90
+ (0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal(Array.from({ length: 250 }, (_, i) => syslog(i)));
91
+ }));
92
+ it("leaves the driver's lines to the test that asks for them", () => __awaiter(this, void 0, void 0, function* () {
93
+ driverLog.line(syslog(1));
94
+ yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
95
+ driverLog.line(syslog(2));
96
+ yield logs.stop(sessionId);
97
+ const forTheTest = yield driverLog.getLogs();
98
+ (0, chai_1.expect)(forTheTest.map((e) => e.message)).to.deep.equal([
99
+ syslog(1),
100
+ syslog(2),
101
+ ]);
102
+ }));
103
+ it("keeps each line's text, so Errors only finds the phone's errors, at the time it arrived", () => __awaiter(this, void 0, void 0, function* () {
104
+ yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
105
+ const before = Date.now();
106
+ driverLog.line(syslog(1));
107
+ driverLog.line(syslog(2, 'Error'));
108
+ driverLog.line(syslog(3, 'Fault'));
109
+ const after = Date.now();
110
+ yield logs.stop(sessionId);
111
+ const rows = yield deviceLogs(sessionId);
112
+ (0, chai_1.expect)(rows.map((r) => { var _a, _b; return (_b = (_a = DASHBOARD_SYSLOG.exec(r.message)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; })).to.deep.equal([
113
+ null,
114
+ 'Error',
115
+ 'Fault',
116
+ ]);
117
+ for (const r of rows) {
118
+ const t = new Date(r.timestamp).getTime();
119
+ (0, chai_1.expect)(t).to.be.within(before, after);
120
+ }
121
+ }));
122
+ it("records a simulator's compact lines, without `log stream`'s banner", () => __awaiter(this, void 0, void 0, function* () {
123
+ driverLog.line('Filtering the log data using "subsystem != \\"com.apple.CoreTelephony\\""');
124
+ driverLog.line('Timestamp Ty Process[PID:TID]');
125
+ yield logs.start({ sessionId, device: SIMULATOR, since: Date.now() - 60000, driverLog });
126
+ driverLog.line('2026-10-05 20:31:40.123 F Shop[4127:8812] Terminating app');
127
+ driverLog.line('\tThread 0 Crashed:');
128
+ yield logs.stop(sessionId);
129
+ (0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
130
+ '2026-10-05 20:31:40.123 F Shop[4127:8812] Terminating app',
131
+ '\tThread 0 Crashed:',
132
+ ]);
133
+ }));
134
+ it("keeps the app's own lines, the errors inside it, the app manager's lines and faults, and says so first", () => __awaiter(this, void 0, void 0, function* () {
135
+ const line = (proc, text, level = 'Notice') => `Oct 5 23:48:33 Rabindras-iPhone ${proc} <${level}>: ${text}`;
136
+ // From the create, before Xenon listens: the app's first line comes
137
+ // before the phone announces its pid.
138
+ driverLog.line(line('Shop[4127]', 'first line'));
139
+ driverLog.line(line('runningboardd(RunningBoard)[34]', 'Acquiring [app<com.example.shop(UUID)>:4127]'));
140
+ yield logs.start({
141
+ sessionId,
142
+ device: IPHONE,
143
+ since: Date.now() - 60000,
144
+ driverLog,
145
+ appUnderTest: 'com.example.shop',
146
+ });
147
+ driverLog.line(line('duetexpertd[412]', 'chatter'));
148
+ driverLog.line(line('Shop(Networking)[4127]', 'request failed', 'Error'));
149
+ driverLog.line(line('wifid[61]', 'link lost', 'Fault'));
150
+ yield logs.stop(sessionId);
151
+ (0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
152
+ "Xenon: Kept here: what the app under test (com.example.shop) logs itself, every error inside it, what the phone says about its launch, state, crashes and end, and faults. The rest of the phone's log is left out.",
153
+ line('Shop[4127]', 'first line'),
154
+ line('runningboardd(RunningBoard)[34]', 'Acquiring [app<com.example.shop(UUID)>:4127]'),
155
+ line('Shop(Networking)[4127]', 'request failed', 'Error'),
156
+ line('wifid[61]', 'link lost', 'Fault'),
157
+ ]);
158
+ }));
159
+ it('records nothing for a session that skips log capture', () => __awaiter(this, void 0, void 0, function* () {
160
+ yield logs.start({ sessionId, device: IPHONE, since: Date.now(), driverLog: undefined });
161
+ (0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(false);
162
+ (0, chai_1.expect)(yield deviceLogs(sessionId)).to.deep.equal([]);
163
+ }));
164
+ it(`holds to the same limit as Android, ${deviceLogBook_1.DEVICE_LOG_LINE_LIMIT} lines then errors`, () => __awaiter(this, void 0, void 0, function* () {
165
+ yield logs.start({ sessionId, device: IPHONE, since: Date.now() - 60000, driverLog });
166
+ for (let i = 0; i < deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 100; i++) {
167
+ driverLog.line(syslog(i, i >= deviceLogBook_1.DEVICE_LOG_LINE_LIMIT && i % 2 === 0 ? 'Error' : 'Notice'));
168
+ }
169
+ yield logs.stop(sessionId);
170
+ const rows = yield deviceLogs(sessionId);
171
+ (0, chai_1.expect)(rows).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1 + 50 + 1);
172
+ (0, chai_1.expect)(rows[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT].message).to.match(/^Xenon: This session reached 10,000/);
173
+ (0, chai_1.expect)(rows[rows.length - 1].message).to.match(/^Xenon: 50 device log lines were left out/);
174
+ }));
175
+ it("doesn't record a node's iPhone, whose driver is the node's", () => {
176
+ (0, chai_1.expect)(logs.appliesTo(IPHONE)).to.equal(true);
177
+ (0, chai_1.expect)(logs.appliesTo(SIMULATOR)).to.equal(true);
178
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, IPHONE), { nodeId: 'a-node' }))).to.equal(false);
179
+ });
180
+ });
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const plugin_1 = require("../../src/plugin");
20
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
21
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
22
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
23
+ const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
24
+ const event_manager_1 = require("../../src/dashboard/event-manager");
25
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
26
+ const device_store_1 = require("../../src/data-service/device-store");
27
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
28
+ const PluginContext_1 = require("../../src/PluginContext");
29
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
30
+ const container_registration_1 = require("../helpers/container-registration");
31
+ const scratch_database_1 = require("../helpers/scratch-database");
32
+ /**
33
+ * A session's device log stops however the session ends on this server, its
34
+ * dashboard on or off: on a node that ends the lines it holds for the hub.
35
+ */
36
+ describe('a session’s device log stops however the session ends', function () {
37
+ this.timeout(30000);
38
+ (0, scratch_database_1.useScratchDatabase)();
39
+ let restore;
40
+ let stop;
41
+ beforeEach(() => {
42
+ restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SessionDeviceLogs_1.SessionDeviceLogs);
43
+ stop = sinon_1.default.stub().resolves();
44
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
45
+ typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, { stop });
46
+ });
47
+ afterEach(() => {
48
+ sinon_1.default.restore();
49
+ restore();
50
+ });
51
+ it('at shutdown', () => __awaiter(this, void 0, void 0, function* () {
52
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
53
+ yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
54
+ (0, chai_1.expect)(stop.calledWith('s-down')).to.equal(true);
55
+ }));
56
+ it('when the driver shuts down unexpectedly on a node', () => __awaiter(this, void 0, void 0, function* () {
57
+ sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').resolves();
58
+ const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
59
+ yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-crash', caps: {} }, null);
60
+ (0, chai_1.expect)(stop.calledWith('s-crash')).to.equal(true);
61
+ }));
62
+ });
63
+ /**
64
+ * After a hub restart, a node session's device log goes on being collected
65
+ * where the hub records sessions: after the newest line it stored.
66
+ */
67
+ describe('a hub restart resumes collecting a node session’s device log', function () {
68
+ this.timeout(30000);
69
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
70
+ let restore;
71
+ let start;
72
+ let savedStore;
73
+ let savedArgs;
74
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
75
+ restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SessionDeviceLogs_1.SessionDeviceLogs);
76
+ start = sinon_1.default.stub().resolves();
77
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { start: sinon_1.default.stub() });
78
+ typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, { start });
79
+ savedArgs = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs;
80
+ savedStore = device_store_1.DeviceStoreFactory._deviceStore;
81
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
82
+ yield scratch.db.session.deleteMany({});
83
+ yield scratch.db.device.deleteMany({});
84
+ const phone = (udid, extra) => scratch.db.device.create({
85
+ data: Object.assign({ udid, host: 'http://127.0.0.1:1', nodeId: 'node-2', platform: 'android', name: udid, sdk: '10', busy: true, userBlocked: false, offline: false }, extra),
86
+ });
87
+ yield phone('node-phone', {});
88
+ yield phone('cloud-phone', {
89
+ cloud: JSON.stringify('browserstack'),
90
+ host: 'https://hub.example.com',
91
+ });
92
+ for (const [id, udid, desired] of [
93
+ ['on-node', 'node-phone', '{}'],
94
+ ['logs-off', 'node-phone', JSON.stringify({ 'xe:save_device_logs': false })],
95
+ ['in-cloud', 'cloud-phone', '{}'],
96
+ ]) {
97
+ yield scratch.db.session.create({
98
+ data: {
99
+ id,
100
+ device_udid: udid,
101
+ device_platform: 'android',
102
+ device_version: '10',
103
+ desired_capabilities: desired,
104
+ session_capabilities: '{}',
105
+ node_id: 'node-2',
106
+ has_live_video: false,
107
+ status: 'running',
108
+ },
109
+ });
110
+ }
111
+ }));
112
+ afterEach(() => {
113
+ for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
114
+ SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
115
+ device_store_1.DeviceStoreFactory._deviceStore = savedStore;
116
+ typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = savedArgs;
117
+ sinon_1.default.restore();
118
+ restore();
119
+ });
120
+ const recover = (enableDashboard) => __awaiter(this, void 0, void 0, function* () {
121
+ typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard });
122
+ yield new SessionManager_1.SessionManager().recoverActiveSessions('hub-1', '/wd/hub');
123
+ // The device log is resumed without waiting on it.
124
+ yield new Promise((r) => setImmediate(r));
125
+ });
126
+ it('resumes a node session’s, through the node, never a cloud provider’s nor one turned off', () => __awaiter(this, void 0, void 0, function* () {
127
+ yield recover(true);
128
+ (0, chai_1.expect)(start.getCalls().map((c) => c.args[0].sessionId)).to.deep.equal(['on-node']);
129
+ const args = start.firstCall.args[0];
130
+ (0, chai_1.expect)(args.resume).to.equal(true);
131
+ (0, chai_1.expect)(typeof args.source.nodeDeviceLogs).to.equal('function');
132
+ }));
133
+ it('resumes none on a hub with its dashboard off, which records no device logs', () => __awaiter(this, void 0, void 0, function* () {
134
+ yield recover(false);
135
+ (0, chai_1.expect)(start.called).to.equal(false);
136
+ }));
137
+ });
@@ -15,65 +15,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const supertest_1 = __importDefault(require("supertest"));
18
- const stream_1 = require("stream");
19
- const LogcatStreamService_1 = require("../../src/device-managers/android/LogcatStreamService");
20
- const PackageResolver_1 = require("../../src/services/logcat/PackageResolver");
21
18
  const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
22
19
  const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
23
20
  const scratch_database_1 = require("../helpers/scratch-database");
24
21
  const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
25
- const pad = (n, w = 2) => String(n).padStart(w, '0');
22
+ const fake_logcat_1 = require("../helpers/fake-logcat");
26
23
  // How the Device logs tab reads a line's level for its "Errors only" filter
27
24
  // (web log-derive.ts `logLineLevel`, whose own tests hold it to lines in the
28
25
  // shape these rows have; the web package can't be imported here).
29
26
  const DASHBOARD_THREADTIME = /^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/;
30
27
  const levelOf = (message) => { var _a, _b; return (_b = (_a = DASHBOARD_THREADTIME.exec(message)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; };
31
- /** A threadtime line logged at `at` by this machine's local clock. */
32
- function lineAt(at, msg, level = 'I', tag = 'Tag', pid = 4127) {
33
- const t = new Date(at);
34
- return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
35
- `${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}\n`);
36
- }
37
- /** An `adb logcat` child whose output the test writes. */
38
- function fakeProc() {
39
- const stdout = new stream_1.PassThrough();
40
- const handlers = {};
41
- return {
42
- stdout,
43
- killed: false,
44
- on(ev, cb) {
45
- (handlers[ev] || (handlers[ev] = [])).push(cb);
46
- },
47
- /** The process ends, as when the phone restarts or is unplugged. */
48
- exit() {
49
- stdout.end();
50
- (handlers.close || []).forEach((h) => h());
51
- },
52
- kill() {
53
- this.killed = true;
54
- },
55
- };
56
- }
57
- /** The real stream service, its `adb logcat` children fake. */
58
- class FakeLogcatStreams extends LogcatStreamService_1.LogcatStreamService {
59
- constructor() {
60
- super(...arguments);
61
- this.procs = [];
62
- }
63
- spawnLogcat() {
64
- return __awaiter(this, void 0, void 0, function* () {
65
- const proc = fakeProc();
66
- this.procs.push(proc);
67
- return proc;
68
- });
69
- }
70
- makeResolver() {
71
- return new PackageResolver_1.PackageResolver(() => __awaiter(this, void 0, void 0, function* () { return ' PID NAME\n 4127 com.example.app\n'; }));
72
- }
73
- get proc() {
74
- return this.procs[this.procs.length - 1];
75
- }
76
- }
77
28
  class TestDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
78
29
  constructor(logcat) {
79
30
  super();
@@ -115,19 +66,6 @@ const PHONE = {
115
66
  host: 'http://127.0.0.1:4723',
116
67
  nodeId: 'this-server',
117
68
  };
118
- /** Lets readline, the stream's package lookups and the mux deliver what was written. */
119
- const settle = () => __awaiter(void 0, void 0, void 0, function* () {
120
- for (let i = 0; i < 5; i++)
121
- yield new Promise((r) => setImmediate(r));
122
- });
123
- const until = (check_1, ...args_1) => __awaiter(void 0, [check_1, ...args_1], void 0, function* (check, ms = 2000) {
124
- const end = Date.now() + ms;
125
- while (!check()) {
126
- if (Date.now() > end)
127
- throw new Error('timed out');
128
- yield new Promise((r) => setTimeout(r, 5));
129
- }
130
- });
131
69
  /** The session's Device logs as the session page gets them. */
132
70
  function deviceLogs(sessionId) {
133
71
  return __awaiter(this, void 0, void 0, function* () {
@@ -159,7 +97,7 @@ describe("An Android session's Device logs", function () {
159
97
  has_live_video: false,
160
98
  },
161
99
  });
162
- logcat = new FakeLogcatStreams();
100
+ logcat = new fake_logcat_1.FakeLogcatStreams();
163
101
  logs = new TestDeviceLogs(logcat);
164
102
  }));
165
103
  afterEach(() => __awaiter(this, void 0, void 0, function* () {
@@ -171,14 +109,14 @@ describe("An Android session's Device logs", function () {
171
109
  yield logs.start({ sessionId, device: PHONE, since });
172
110
  // History from before the session, as `logcat -T` sends it first.
173
111
  for (let i = 0; i < 20; i++)
174
- logcat.proc.stdout.write(lineAt(since - 600000 + i, `old ${i}`));
112
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since - 600000 + i, `old ${i}`));
175
113
  // 250 lines over the run, a batch between each "command".
176
114
  for (let batch = 0; batch < 5; batch++) {
177
115
  for (let i = 0; i < 50; i++) {
178
116
  const n = batch * 50 + i;
179
- logcat.proc.stdout.write(lineAt(since + 1000 + n, `line ${n}`));
117
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000 + n, `line ${n}`));
180
118
  }
181
- yield settle();
119
+ yield (0, fake_logcat_1.settle)();
182
120
  }
183
121
  yield logs.stop(sessionId);
184
122
  const rows = yield deviceLogs(sessionId);
@@ -188,11 +126,11 @@ describe("An Android session's Device logs", function () {
188
126
  const since = Date.now() - 60000;
189
127
  yield logs.start({ sessionId, device: PHONE, since });
190
128
  const crashAt = since + 30123;
191
- logcat.proc.stdout.write(lineAt(crashAt - 5, 'Start proc com.example.app', 'I', 'ActivityManager'));
192
- logcat.proc.stdout.write(lineAt(crashAt, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
193
- logcat.proc.stdout.write(lineAt(crashAt, 'Process: com.example.app, PID: 4127', 'E', 'AndroidRuntime'));
194
- logcat.proc.stdout.write(lineAt(crashAt + 1, 'Sending signal. PID: 4127 SIG: 9', 'I', 'Process'));
195
- yield settle();
129
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt - 5, 'Start proc com.example.app', 'I', 'ActivityManager'));
130
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
131
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt, 'Process: com.example.app, PID: 4127', 'E', 'AndroidRuntime'));
132
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(crashAt + 1, 'Sending signal. PID: 4127 SIG: 9', 'I', 'Process'));
133
+ yield (0, fake_logcat_1.settle)();
196
134
  yield logs.stop(sessionId);
197
135
  const rows = yield deviceLogs(sessionId);
198
136
  (0, chai_1.expect)(rows.map((r) => levelOf(r.message))).to.deep.equal(['I', 'E', 'E', 'I']);
@@ -210,8 +148,8 @@ describe("An Android session's Device logs", function () {
210
148
  const at = since + 10000;
211
149
  const frames = Array.from({ length: 40 }, (_, i) => `\tat com.example.Frame${i}.run(Frame.java:${i})`);
212
150
  for (const frame of frames)
213
- logcat.proc.stdout.write(lineAt(at, frame, 'E', 'AndroidRuntime'));
214
- yield settle();
151
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(at, frame, 'E', 'AndroidRuntime'));
152
+ yield (0, fake_logcat_1.settle)();
215
153
  yield logs.stop(sessionId);
216
154
  const rows = yield deviceLogs(sessionId);
217
155
  (0, chai_1.expect)(rows.map((r) => r.message.replace(/^.*AndroidRuntime: /, ''))).to.deep.equal(frames);
@@ -224,14 +162,14 @@ describe("An Android session's Device logs", function () {
224
162
  const since = Date.now() - 60000;
225
163
  yield logs.start({ sessionId, device: PHONE, since });
226
164
  for (let i = 0; i < SessionDeviceLogs_1.WRITE_BATCH + 10; i++)
227
- logcat.proc.stdout.write(lineAt(since + i, `line ${i}`));
228
- yield settle();
165
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + i, `line ${i}`));
166
+ yield (0, fake_logcat_1.settle)();
229
167
  // A full batch is written while the session runs, without waiting for
230
168
  // its end or the interval (a minute here).
231
169
  const written = () => scratch.db.log.count({ where: { session_id: sessionId } });
232
170
  const end = Date.now() + 2000;
233
171
  while ((yield written()) < SessionDeviceLogs_1.WRITE_BATCH && Date.now() < end)
234
- yield settle();
172
+ yield (0, fake_logcat_1.settle)();
235
173
  (0, chai_1.expect)(yield written()).to.be.at.least(SessionDeviceLogs_1.WRITE_BATCH);
236
174
  (0, chai_1.expect)(logs.isRecording(sessionId)).to.equal(true);
237
175
  yield logs.stop(sessionId);
@@ -243,18 +181,18 @@ describe("An Android session's Device logs", function () {
243
181
  const since = Date.now() - 60000;
244
182
  yield logs.start({ sessionId, device: PHONE, since });
245
183
  const first = logcat.proc;
246
- first.stdout.write(lineAt(since + 1000, 'before 1'));
247
- first.stdout.write(lineAt(since + 2000, 'before 2'));
248
- yield settle();
184
+ first.stdout.write((0, fake_logcat_1.lineAt)(since + 1000, 'before 1'));
185
+ first.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'before 2'));
186
+ yield (0, fake_logcat_1.settle)();
249
187
  first.exit();
250
- yield until(() => logcat.procs.length === 2);
188
+ yield (0, fake_logcat_1.until)(() => logcat.procs.length === 2);
251
189
  // The new stream starts with history the session already has, then a
252
190
  // line of the same millisecond it hadn't seen, then new lines.
253
- logcat.proc.stdout.write(lineAt(since + 1000, 'before 1'));
254
- logcat.proc.stdout.write(lineAt(since + 2000, 'before 2'));
255
- logcat.proc.stdout.write(lineAt(since + 2000, 'also at 2'));
256
- logcat.proc.stdout.write(lineAt(since + 9000, 'after'));
257
- yield settle();
191
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000, 'before 1'));
192
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'before 2'));
193
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'also at 2'));
194
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 9000, 'after'));
195
+ yield (0, fake_logcat_1.settle)();
258
196
  yield logs.stop(sessionId);
259
197
  (0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal([
260
198
  'before 1',
@@ -271,8 +209,8 @@ describe("An Android session's Device logs", function () {
271
209
  mux.addClient((r) => viewer.push(r.message), () => true);
272
210
  yield logs.start({ sessionId, device: PHONE, since });
273
211
  (0, chai_1.expect)(logcat.procs).to.have.length(1);
274
- logcat.proc.stdout.write(lineAt(since + 1, 'shared'));
275
- yield settle();
212
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1, 'shared'));
213
+ yield (0, fake_logcat_1.settle)();
276
214
  yield logs.stop(sessionId);
277
215
  (0, chai_1.expect)(viewer).to.deep.equal(['shared']);
278
216
  (0, chai_1.expect)(logcat.proc.killed).to.equal(false);
@@ -294,14 +232,14 @@ describe("An Android session's Device logs", function () {
294
232
  for (let i = 0; i < total; i++) {
295
233
  // Past the limit, five lines in six are errors: 2,500 errors, 500 others.
296
234
  const level = i >= deviceLogBook_1.DEVICE_LOG_LINE_LIMIT && i % 6 !== 0 ? 'E' : 'D';
297
- chunk += lineAt(since + 1000 + i, `line ${i}`, level);
235
+ chunk += (0, fake_logcat_1.lineAt)(since + 1000 + i, `line ${i}`, level);
298
236
  if (i % 1000 === 999) {
299
237
  logcat.proc.stdout.write(chunk);
300
238
  chunk = '';
301
- yield settle();
239
+ yield (0, fake_logcat_1.settle)();
302
240
  }
303
241
  }
304
- yield settle();
242
+ yield (0, fake_logcat_1.settle)();
305
243
  yield logs.stop(sessionId);
306
244
  const rows = yield deviceLogs(sessionId);
307
245
  (0, chai_1.expect)(rows).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1 + deviceLogBook_1.DEVICE_LOG_ERROR_LIMIT + 1);
@@ -311,10 +249,12 @@ describe("An Android session's Device logs", function () {
311
249
  (0, chai_1.expect)(errors.every((r) => levelOf(r.message) === 'E')).to.equal(true);
312
250
  (0, chai_1.expect)(rows[rows.length - 1].message).to.equal("Xenon: 1,000 device log lines were left out to keep this session's log within its limit.");
313
251
  }));
314
- it("only records this server's own Android phones", () => {
252
+ it("only records this server's own phones", () => {
315
253
  (0, chai_1.expect)(logs.appliesTo(PHONE)).to.equal(true);
316
254
  (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { nodeId: 'a-node' }))).to.equal(false);
317
- (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'ios' }))).to.equal(false);
255
+ // An iPhone is recorded from its driver's log (session-device-logs-ios.spec.ts).
256
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'ios' }))).to.equal(true);
257
+ (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { platform: 'windows' }))).to.equal(false);
318
258
  (0, chai_1.expect)(logs.appliesTo(Object.assign(Object.assign({}, PHONE), { cloud: true }))).to.equal(false);
319
259
  (0, chai_1.expect)(logs.appliesTo(undefined)).to.equal(false);
320
260
  });
@@ -322,12 +262,19 @@ describe("An Android session's Device logs", function () {
322
262
  logs.clock = null;
323
263
  const since = Date.now() - 60000;
324
264
  yield logs.start({ sessionId, device: PHONE, since });
325
- logcat.proc.stdout.write(lineAt(since - 600000, 'before'));
326
- logcat.proc.stdout.write(lineAt(since + 10, 'during'));
327
- yield settle();
265
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since - 600000, 'before'));
266
+ logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 10, 'during'));
267
+ yield (0, fake_logcat_1.settle)();
328
268
  yield logs.stop(sessionId);
329
269
  (0, chai_1.expect)(textOf(yield deviceLogs(sessionId))).to.deep.equal(['during']);
330
270
  }));
271
+ it('says in the log when the session turned it off', () => __awaiter(this, void 0, void 0, function* () {
272
+ yield logs.noteOff(sessionId);
273
+ (0, chai_1.expect)((yield deviceLogs(sessionId)).map((r) => r.message)).to.deep.equal([
274
+ "Xenon: This session's device log wasn't kept: the test turned it off.",
275
+ ]);
276
+ (0, chai_1.expect)(logcat.procs).to.have.length(0);
277
+ }));
331
278
  it('stops however often it is told to', () => __awaiter(this, void 0, void 0, function* () {
332
279
  yield logs.start({ sessionId, device: PHONE, since: Date.now() });
333
280
  yield logs.stop(sessionId);
@@ -100,10 +100,10 @@ describe('ending a failed session', () => {
100
100
  notify: false,
101
101
  });
102
102
  it('returns while the AI is still explaining the failure', () => __awaiter(void 0, void 0, void 0, function* () {
103
- (0, chai_1.expect)(yield within(2000, end())).to.equal('ended');
103
+ (0, chai_1.expect)(yield within(10000, end())).to.equal('ended');
104
104
  // The AI is still asked, once the session has ended.
105
105
  const analyzeFailure = AIService_1.AI_SERVICE.analyzeFailure;
106
- for (let i = 0; i < 100 && !analyzeFailure.called; i++) {
106
+ for (let i = 0; i < 500 && !analyzeFailure.called; i++) {
107
107
  yield new Promise((r) => setTimeout(r, 20));
108
108
  }
109
109
  (0, chai_1.expect)(analyzeFailure.calledOnce).to.equal(true);
@@ -113,17 +113,17 @@ describe('ending a failed session', () => {
113
113
  });
114
114
  }));
115
115
  it('has filed the session under its category when it returns', () => __awaiter(void 0, void 0, void 0, function* () {
116
- yield within(2000, end());
116
+ yield within(10000, end());
117
117
  const saved = yield row();
118
118
  (0, chai_1.expect)(saved.status).to.equal(SessionStatus_1.SessionStatus.FAILED);
119
119
  (0, chai_1.expect)(saved.failure_category).to.equal('ELEMENT_NOT_FOUND');
120
120
  (0, chai_1.expect)(saved.ai_analysis).to.equal(null);
121
121
  }));
122
122
  it("saves the AI's explanation when it comes", () => __awaiter(void 0, void 0, void 0, function* () {
123
- yield within(2000, end());
123
+ yield within(10000, end());
124
124
  explain.resolve('Root Cause: the Login button was covered by a dialog.');
125
125
  let saved = yield row();
126
- for (let i = 0; i < 100 && !saved.ai_analysis; i++) {
126
+ for (let i = 0; i < 500 && !saved.ai_analysis; i++) {
127
127
  yield new Promise((r) => setTimeout(r, 20));
128
128
  saved = yield row();
129
129
  }
@@ -18,6 +18,7 @@ const supertest_1 = __importDefault(require("supertest"));
18
18
  const typedi_1 = require("typedi");
19
19
  const CleanupService_1 = require("../../src/services/CleanupService");
20
20
  const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
21
+ const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
21
22
  const scratch_database_1 = require("../helpers/scratch-database");
22
23
  const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
23
24
  /** The SQL statements `run` makes. */
@@ -145,6 +146,20 @@ describe("A session's logs and profiling are read through an index", function ()
145
146
  (0, chai_1.expect)(scansOf(table, steps), steps.join('\n')).to.deep.equal([]);
146
147
  }));
147
148
  }
149
+ it("finds a session's newest device log lines without a scan, as a hub resumes after a restart", () => __awaiter(this, void 0, void 0, function* () {
150
+ class Reader extends SessionDeviceLogs_1.SessionDeviceLogs {
151
+ tail(sessionId) {
152
+ return this.storedTail(sessionId);
153
+ }
154
+ }
155
+ let tail = [];
156
+ const steps = yield stepsOn(scratch, 'Log', yield sqlOf(scratch, () => __awaiter(this, void 0, void 0, function* () {
157
+ tail = yield new Reader().tail('ix-session');
158
+ })));
159
+ (0, chai_1.expect)(tail.map((l) => l.message)).to.deep.equal(['I/ActivityManager: start']);
160
+ (0, chai_1.expect)(steps, 'no Log statement was captured').to.not.be.empty;
161
+ (0, chai_1.expect)(scansOf('Log', steps), steps.join('\n')).to.deep.equal([]);
162
+ }));
148
163
  it("deletes a session's commands, logs and profiling without a scan", () => __awaiter(this, void 0, void 0, function* () {
149
164
  const ran = yield sqlOf(scratch, () => typedi_1.Container.get(CleanupService_1.CleanupService).purgeBuild('ix-build', false));
150
165
  for (const table of ['SessionLog', 'Log', 'Profiling']) {
@@ -199,7 +214,7 @@ describe('The selector verification reads SessionLog through an index', function
199
214
  });
200
215
  }));
201
216
  it("reads a selector's finds without a scan", () => __awaiter(this, void 0, void 0, function* () {
202
- const socket = { emitToDashboard: () => undefined };
217
+ const socket = { emitToDashboardForSelector: () => __awaiter(this, void 0, void 0, function* () { return undefined; }) };
203
218
  const ran = yield sqlOf(scratch, () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run());
204
219
  const steps = yield stepsOn(scratch, 'SessionLog', ran);
205
220
  (0, chai_1.expect)(steps, 'no SessionLog statement was captured').to.not.be.empty;