@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
@@ -73,6 +73,10 @@ function harness(over) {
73
73
  const mux = new LogcatMultiplexer_1.LogcatMultiplexer();
74
74
  const state = { startCalls: 0, authorizeCalls: 0, authorizeActors: [] };
75
75
  const server = http_1.default.createServer();
76
+ const connectionsClosed = [];
77
+ server.on('connection', (socket) => {
78
+ connectionsClosed.push(new Promise((r) => socket.once('close', () => r())));
79
+ });
76
80
  const userAuthorize = (_a = over.authorize) !== null && _a !== void 0 ? _a : (() => __awaiter(this, void 0, void 0, function* () { return true; }));
77
81
  const deps = {
78
82
  redeem: (_b = over.redeem) !== null && _b !== void 0 ? _b : (() => __awaiter(this, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); })),
@@ -109,6 +113,13 @@ function harness(over) {
109
113
  get authorizeActors() {
110
114
  return state.authorizeActors;
111
115
  },
116
+ serverSawClose: () => __awaiter(this, void 0, void 0, function* () {
117
+ yield Promise.all(connectionsClosed);
118
+ // ws emits the server WebSocket's 'close' from the TCP socket's 'close'
119
+ // listener, or from a process.nextTick after it (its receiver's
120
+ // 'finish'). Either has run by the event loop's next check phase.
121
+ yield new Promise((r) => setImmediate(r));
122
+ }),
112
123
  close: () => new Promise((r) => {
113
124
  var _a;
114
125
  // http.Server#close only fires its callback once every connection has
@@ -159,6 +170,50 @@ function connectCollecting(h_1) {
159
170
  return { code, got };
160
171
  });
161
172
  }
173
+ /**
174
+ * Holds one handshake step until the test releases it, and says when the
175
+ * server has reached it, so a test acts while that step is in flight instead
176
+ * of guessing how long the steps before it take. Under load a fixed 50 ms
177
+ * wasn't enough: the client was still CONNECTING, and terminate() there
178
+ * raised an uncaught error.
179
+ */
180
+ function heldStep() {
181
+ let enter = () => undefined;
182
+ let release = () => undefined;
183
+ const entered = new Promise((r) => (enter = r));
184
+ const released = new Promise((r) => (release = r));
185
+ return {
186
+ entered,
187
+ release,
188
+ /** For the stubbed step: marks it entered, then waits for the release. */
189
+ hold: () => __awaiter(this, void 0, void 0, function* () {
190
+ enter();
191
+ yield released;
192
+ }),
193
+ };
194
+ }
195
+ /**
196
+ * Connects, drops the connection once it is open and `step` is in flight,
197
+ * and resolves when the server has seen it go.
198
+ */
199
+ function dropDuring(h, step) {
200
+ return __awaiter(this, void 0, void 0, function* () {
201
+ const ws = new ws_1.WebSocket(url(h.port));
202
+ // terminate() waits for 'open', so it never raises the 'error' it raises on
203
+ // a CONNECTING socket. Any other unhandled 'error' would be an uncaught
204
+ // exception, failing whichever test is running when it lands.
205
+ ws.on('error', () => undefined);
206
+ yield Promise.all([new Promise((r) => ws.once('open', r)), step.entered]);
207
+ ws.terminate();
208
+ yield h.serverSawClose();
209
+ });
210
+ }
211
+ /**
212
+ * Lets the handshake run on after a held step is released. Everything after
213
+ * a step is promise continuations (the harness's steps do no I/O), so it has
214
+ * all run by the event loop's next check phase.
215
+ */
216
+ const handshakeSettled = () => new Promise((r) => setImmediate(r));
162
217
  describe('attachLogcatWs handshake', () => {
163
218
  it('accepts a valid ticket for a device the caller may use', () => __awaiter(void 0, void 0, void 0, function* () {
164
219
  const h = yield harness({});
@@ -265,26 +320,21 @@ describe('attachLogcatWs handshake', () => {
265
320
  // The trap documented in h264StreamWs: without cleanup registered before the
266
321
  // awaits, clientCount stays inflated and the idle watchdog never fires.
267
322
  it('leaves no client registered when the socket drops mid-handshake during redeem', () => __awaiter(void 0, void 0, void 0, function* () {
268
- let release = () => undefined;
269
- const gate = new Promise((r) => (release = r));
323
+ const step = heldStep();
270
324
  const h = yield harness({
271
325
  redeem: () => __awaiter(void 0, void 0, void 0, function* () {
272
- yield gate;
326
+ yield step.hold();
273
327
  return { actorId: 'usr_alice' };
274
328
  }),
275
329
  });
276
- const ws = new ws_1.WebSocket(url(h.port));
277
- yield new Promise((r) => setTimeout(r, 50));
278
- ws.terminate(); // disconnect while redeem is still pending
279
- // Let the server actually observe the close (a real socket teardown is a
280
- // macrotask; resolving `gate` below is a same-tick microtask chain that
281
- // would otherwise race ahead of it) BEFORE unblocking redeem. Otherwise
282
- // this proves nothing about the post-redeem guard: the client could get
283
- // registered first and only be torn down afterward by the 'close'
284
- // listener firing late — same end state, different (unguarded) mechanism.
285
- yield new Promise((r) => setTimeout(r, 100));
286
- release();
287
- yield new Promise((r) => setTimeout(r, 150));
330
+ // Disconnect while redeem is still pending, and let the server see the
331
+ // close BEFORE redeem resolves. Otherwise this proves nothing about the
332
+ // post-redeem guard: the client could get registered first and only be
333
+ // torn down afterward by the 'close' listener firing late — same end
334
+ // state, different (unguarded) mechanism.
335
+ yield dropDuring(h, step);
336
+ step.release();
337
+ yield handshakeSettled();
288
338
  (0, chai_1.expect)(h.mux.clientCount).to.equal(0);
289
339
  // The final clientCount alone can't distinguish "the post-redeem guard
290
340
  // short-circuited" from "a later guard cleaned up after the fact" — both
@@ -301,20 +351,16 @@ describe('attachLogcatWs handshake', () => {
301
351
  // session owner) when the caller disconnects. Same reasoning as the two
302
352
  // neighbours — the observable is the adb spawn that must not happen.
303
353
  it('leaves no client registered when the socket drops mid-handshake during authorize', () => __awaiter(void 0, void 0, void 0, function* () {
304
- let release = () => undefined;
305
- const gate = new Promise((r) => (release = r));
354
+ const step = heldStep();
306
355
  const h = yield harness({
307
356
  authorize: () => __awaiter(void 0, void 0, void 0, function* () {
308
- yield gate;
357
+ yield step.hold();
309
358
  return true;
310
359
  }),
311
360
  });
312
- const ws = new ws_1.WebSocket(url(h.port));
313
- yield new Promise((r) => setTimeout(r, 50));
314
- ws.terminate(); // disconnect while authorize is still pending
315
- yield new Promise((r) => setTimeout(r, 100));
316
- release();
317
- yield new Promise((r) => setTimeout(r, 150));
361
+ yield dropDuring(h, step); // disconnect while authorize is still pending
362
+ step.release();
363
+ yield handshakeSettled();
318
364
  (0, chai_1.expect)(h.authorizeCalls, 'authorize was already in flight').to.equal(1);
319
365
  (0, chai_1.expect)(h.startCalls, 'must not start a logcat process once the caller is gone').to.equal(0);
320
366
  (0, chai_1.expect)(h.mux.clientCount).to.equal(0);
@@ -324,25 +370,20 @@ describe('attachLogcatWs handshake', () => {
324
370
  // but startStream (which can shell out to adb) is still in flight when the
325
371
  // caller disconnects.
326
372
  it('leaves no client registered when the socket drops mid-handshake during startStream', () => __awaiter(void 0, void 0, void 0, function* () {
327
- let release = () => undefined;
328
- const gate = new Promise((r) => (release = r));
373
+ const step = heldStep();
329
374
  const gatedMux = new LogcatMultiplexer_1.LogcatMultiplexer();
330
375
  const h = yield harness({
331
376
  startStream: () => __awaiter(void 0, void 0, void 0, function* () {
332
- yield gate;
377
+ yield step.hold();
333
378
  return { mux: gatedMux };
334
379
  }),
335
380
  });
336
- const ws = new ws_1.WebSocket(url(h.port));
337
- yield new Promise((r) => setTimeout(r, 50));
338
- ws.terminate(); // disconnect while startStream is still pending
339
- // See the matching comment in the redeem-timing test above: give the
340
- // server's close listener time to actually run before startStream
341
- // resolves, or this races the wrong way and proves nothing about the
342
- // post-startStream guard.
343
- yield new Promise((r) => setTimeout(r, 100));
344
- release();
345
- yield new Promise((r) => setTimeout(r, 150));
381
+ // As in the redeem test above: the server sees the close before
382
+ // startStream resolves, or this proves nothing about the post-startStream
383
+ // guard.
384
+ yield dropDuring(h, step);
385
+ step.release();
386
+ yield handshakeSettled();
346
387
  (0, chai_1.expect)(gatedMux.clientCount).to.equal(0);
347
388
  yield h.close();
348
389
  }));
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
8
+ const logger_1 = require("../../src/logger");
9
+ /**
10
+ * In JSON log mode every line was written through Appium's `info`, whatever
11
+ * its level: a debug line printed at `--log-level info`, and an error went to
12
+ * stdout, since Appium's console sends only its `error` level to stderr. Each
13
+ * line now goes out at its own level, as in text mode.
14
+ */
15
+ describe('XenonLogger in JSON mode writes each line at its own level', () => {
16
+ let wasJson;
17
+ let base;
18
+ let logger;
19
+ beforeEach(() => {
20
+ wasJson = logger_1.XenonLogger.isJsonLogging;
21
+ logger_1.XenonLogger.configure({ enableJsonLogging: true });
22
+ base = {
23
+ debug: sinon_1.default.stub(),
24
+ info: sinon_1.default.stub(),
25
+ warn: sinon_1.default.stub(),
26
+ error: sinon_1.default.stub(),
27
+ };
28
+ logger = new logger_1.XenonLogger('probe');
29
+ logger.baseLogger = base;
30
+ });
31
+ afterEach(() => {
32
+ logger_1.XenonLogger.configure({ enableJsonLogging: wasJson });
33
+ });
34
+ for (const level of ['debug', 'info', 'warn', 'error']) {
35
+ it(level, () => {
36
+ logger[level](`a ${level} line`);
37
+ const written = Object.keys(base).filter((l) => base[l].called);
38
+ (0, chai_1.expect)(written).to.deep.equal([level]);
39
+ const entry = JSON.parse(base[level].firstCall.args[0]);
40
+ (0, chai_1.expect)(entry.level).to.equal(level);
41
+ (0, chai_1.expect)(entry.message).to.equal(`a ${level} line`);
42
+ });
43
+ }
44
+ });
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
8
+ const mcpTokenTtl_1 = require("../../src/services/token/mcpTokenTtl");
9
+ /**
10
+ * XENON_MCP_TOKEN_TTL_SEC=abc made every xenon-mcp token's lifetime NaN.
11
+ * A value that isn't a whole number of seconds above 0 now warns and keeps
12
+ * the default.
13
+ */
14
+ describe('XENON_MCP_TOKEN_TTL_SEC', () => {
15
+ it('is the default (a day) when unset or empty, without a warning', () => {
16
+ const warn = sinon_1.default.stub();
17
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(undefined, warn)).to.equal(86400);
18
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)('', warn)).to.equal(86400);
19
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(' ', warn)).to.equal(86400);
20
+ (0, chai_1.expect)(mcpTokenTtl_1.DEFAULT_MCP_TOKEN_TTL_SEC).to.equal(86400);
21
+ (0, chai_1.expect)(warn.called).to.equal(false);
22
+ });
23
+ it('is the number of seconds it gives', () => {
24
+ const warn = sinon_1.default.stub();
25
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)('3600', warn)).to.equal(3600);
26
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(' 120 ', warn)).to.equal(120);
27
+ (0, chai_1.expect)(warn.called).to.equal(false);
28
+ });
29
+ for (const bad of ['abc', '0', '-60', '1.5', '1e400', 'Infinity', '12s']) {
30
+ it(`warns and keeps the default for "${bad}"`, () => {
31
+ const warn = sinon_1.default.stub();
32
+ (0, chai_1.expect)((0, mcpTokenTtl_1.mcpTokenTtlSec)(bad, warn)).to.equal(86400);
33
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
34
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.include('XENON_MCP_TOKEN_TTL_SEC');
35
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.include(bad);
36
+ });
37
+ }
38
+ });
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ require("reflect-metadata");
7
+ const chai_1 = require("chai");
8
+ const sinon_1 = __importDefault(require("sinon"));
9
+ const NodeDeviceLogStore_1 = require("../../src/services/logcat/NodeDeviceLogStore");
10
+ const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
11
+ const row = (n) => ({ message: `line ${n}`, timestamp: new Date(1000 + n) });
12
+ const rows = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => row(from + i));
13
+ describe('NodeDeviceLogStore: a node holds a session’s device log lines for its hub', () => {
14
+ let now;
15
+ let store;
16
+ beforeEach(() => {
17
+ now = 1000000;
18
+ store = new NodeDeviceLogStore_1.NodeDeviceLogStore();
19
+ store.now = () => now;
20
+ });
21
+ it('numbers each line, and answers every one without `after`', () => {
22
+ store.begin('s1');
23
+ store.add('s1', [row(1), row(2)]);
24
+ store.add('s1', [row(3)]);
25
+ (0, chai_1.expect)(store.read('s1', null)).to.deep.equal({
26
+ state: 'recording',
27
+ more: false,
28
+ lines: [
29
+ { seq: 1, message: 'line 1', timestamp: 1001 },
30
+ { seq: 2, message: 'line 2', timestamp: 1002 },
31
+ { seq: 3, message: 'line 3', timestamp: 1003 },
32
+ ],
33
+ });
34
+ });
35
+ it('drops what the hub has: the lines at or before `after`', () => {
36
+ store.begin('s1');
37
+ store.add('s1', rows(1, 3));
38
+ (0, chai_1.expect)(store.read('s1', 2).lines.map((l) => l.seq)).to.deep.equal([3]);
39
+ // Gone for good, even to an ask that names none.
40
+ (0, chai_1.expect)(store.read('s1', null).lines.map((l) => l.seq)).to.deep.equal([3]);
41
+ store.add('s1', [row(4)]);
42
+ (0, chai_1.expect)(store.read('s1', 3).lines.map((l) => l.seq)).to.deep.equal([4]);
43
+ });
44
+ it(`answers at most ${nodeDeviceLogs_1.NODE_DEVICE_LOG_PAGE} lines at a time, and says there are more`, () => {
45
+ store.begin('s1');
46
+ store.add('s1', rows(1, nodeDeviceLogs_1.NODE_DEVICE_LOG_PAGE + 5));
47
+ const first = store.read('s1', null);
48
+ (0, chai_1.expect)(first.lines).to.have.length(nodeDeviceLogs_1.NODE_DEVICE_LOG_PAGE);
49
+ (0, chai_1.expect)(first.more).to.equal(true);
50
+ const second = store.read('s1', nodeDeviceLogs_1.NODE_DEVICE_LOG_PAGE);
51
+ (0, chai_1.expect)(second.lines.map((l) => l.seq)).to.deep.equal([1, 2, 3, 4, 5].map((n) => nodeDeviceLogs_1.NODE_DEVICE_LOG_PAGE + n));
52
+ (0, chai_1.expect)(second.more).to.equal(false);
53
+ });
54
+ it('says a session ended, and keeps it 10 minutes for the hub’s last ask', () => {
55
+ store.begin('s1');
56
+ store.add('s1', rows(1, 2));
57
+ store.end('s1');
58
+ (0, chai_1.expect)(store.read('s1', null).state).to.equal('ended');
59
+ now += nodeDeviceLogs_1.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS - 1;
60
+ (0, chai_1.expect)(store.read('s1', 1).lines.map((l) => l.seq)).to.deep.equal([2]);
61
+ now += 1;
62
+ (0, chai_1.expect)(store.read('s1', null)).to.deep.equal({ state: 'off', lines: [], more: false });
63
+ });
64
+ it('forgets an ended session 10 minutes on, though nothing else happens', () => {
65
+ const clock = sinon_1.default.useFakeTimers({ now });
66
+ try {
67
+ const timed = new NodeDeviceLogStore_1.NodeDeviceLogStore();
68
+ timed.begin('s1');
69
+ timed.add('s1', rows(1, 3));
70
+ timed.end('s1');
71
+ clock.tick(nodeDeviceLogs_1.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS);
72
+ (0, chai_1.expect)(timed.entries.has('s1')).to.equal(false);
73
+ }
74
+ finally {
75
+ clock.restore();
76
+ }
77
+ });
78
+ it('forgets an ended session nobody asks about, at the next session', () => {
79
+ store.begin('s1');
80
+ store.end('s1');
81
+ now += nodeDeviceLogs_1.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS;
82
+ store.begin('s2');
83
+ (0, chai_1.expect)(store.entries.has('s1')).to.equal(false);
84
+ });
85
+ it('says off for a session it holds nothing for, and keeps nothing for it', () => {
86
+ store.add('nobody', rows(1, 2));
87
+ (0, chai_1.expect)(store.read('nobody', null)).to.deep.equal({ state: 'off', lines: [], more: false });
88
+ });
89
+ it('knows whether a hub has asked about a session', () => {
90
+ store.begin('s1');
91
+ (0, chai_1.expect)(store.claimed('s1')).to.equal(false);
92
+ store.read('s1', null);
93
+ (0, chai_1.expect)(store.claimed('s1')).to.equal(true);
94
+ (0, chai_1.expect)(store.claimed('unknown')).to.equal(false);
95
+ });
96
+ it('forgets a session it was told to', () => {
97
+ store.begin('s1');
98
+ store.add('s1', rows(1, 2));
99
+ store.forget('s1');
100
+ (0, chai_1.expect)(store.read('s1', null).state).to.equal('off');
101
+ });
102
+ });