@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
@@ -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,25 @@
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 fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ /**
11
+ * The example Loki config set `limits_config.retention_period: 24h`, but Loki
12
+ * deletes nothing unless its compactor runs retention, so the logs it kept
13
+ * grew for ever. Loki 3 also refuses to start with retention enabled and no
14
+ * store for delete requests.
15
+ */
16
+ describe('examples/observability/loki.yml', () => {
17
+ const config = js_yaml_1.default.load(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../examples/observability/loki.yml'), 'utf8'));
18
+ it('keeps logs for the retention period it sets', () => {
19
+ var _a, _b, _c;
20
+ (0, chai_1.expect)(config.limits_config.retention_period).to.equal('24h');
21
+ (0, chai_1.expect)((_a = config.compactor) === null || _a === void 0 ? void 0 : _a.retention_enabled, 'compactor.retention_enabled').to.equal(true);
22
+ (0, chai_1.expect)((_b = config.compactor) === null || _b === void 0 ? void 0 : _b.delete_request_store, 'compactor.delete_request_store').to.equal(config.schema_config.configs[0].object_store);
23
+ (0, chai_1.expect)((_c = config.compactor) === null || _c === void 0 ? void 0 : _c.working_directory, 'compactor.working_directory').to.be.a('string');
24
+ });
25
+ });
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ // scripts/ is plain CommonJS that the export script also loads, so it is
5
+ // required rather than imported.
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ const site = require('../../scripts/lib/openapi-for-site');
8
+ const { forSite, VERSION_TOKEN, assertNoRawDocumentLink } = site;
9
+ /**
10
+ * The documentation site publishes the server's OpenAPI document. The spec
11
+ * names the version it was built from in `info`, so a committed copy would
12
+ * change on every release. forSite swaps that version for a token the site's
13
+ * build fills in, and points the "raw document" link at the user's own server,
14
+ * since the link would otherwise lead to the docs host.
15
+ */
16
+ describe('openapi for the documentation site', () => {
17
+ const version = '2.13.1';
18
+ const makeSpec = () => ({
19
+ info: {
20
+ version,
21
+ description: `This reference describes version **${version}**. The raw OpenAPI document is at [\`/xenon/api-docs.json\`](/xenon/api-docs.json).`,
22
+ },
23
+ paths: { '/api/devices': {} },
24
+ });
25
+ it('puts the version token where the version was', () => {
26
+ const result = forSite(makeSpec(), version);
27
+ (0, chai_1.expect)(result.info.version).to.equal('__XENON_VERSION__');
28
+ (0, chai_1.expect)(VERSION_TOKEN).to.equal('__XENON_VERSION__');
29
+ (0, chai_1.expect)(result.info.description).to.contain('version **__XENON_VERSION__**');
30
+ });
31
+ it('leaves no trace of the version anywhere in the result', () => {
32
+ (0, chai_1.expect)(JSON.stringify(forSite(makeSpec(), version))).to.not.contain(version);
33
+ });
34
+ it('points the raw-document line at the reader’s own server, with no link', () => {
35
+ const { description } = forSite(makeSpec(), version).info;
36
+ (0, chai_1.expect)(description).to.contain('`/xenon/api-docs.json` on your server');
37
+ (0, chai_1.expect)(description).to.not.contain('](/xenon/api-docs.json)');
38
+ });
39
+ it('keeps the paths exactly as they were', () => {
40
+ (0, chai_1.expect)(forSite(makeSpec(), version).paths).to.deep.equal(makeSpec().paths);
41
+ });
42
+ it('does not change the object it is given', () => {
43
+ const spec = makeSpec();
44
+ forSite(spec, version);
45
+ (0, chai_1.expect)(spec).to.deep.equal(makeSpec());
46
+ });
47
+ it('refuses an empty or missing version', () => {
48
+ (0, chai_1.expect)(() => forSite(makeSpec(), '')).to.throw(/version/);
49
+ (0, chai_1.expect)(() => forSite(makeSpec(), undefined)).to.throw(/version/);
50
+ (0, chai_1.expect)(() => forSite(makeSpec(), 2.13)).to.throw(/version/);
51
+ });
52
+ it('only touches the version inside info', () => {
53
+ const spec = makeSpec();
54
+ spec.paths['/api/version'] = { get: { example: { pluginVersion: version } } };
55
+ const result = forSite(spec, version);
56
+ (0, chai_1.expect)(result.paths['/api/version'].get.example.pluginVersion).to.equal(version);
57
+ });
58
+ describe('the guard against a link to the raw document', () => {
59
+ const guarded = (description) => {
60
+ const spec = makeSpec();
61
+ spec.info.description = description;
62
+ return JSON.stringify(forSite(spec, version), null, 2);
63
+ };
64
+ it('passes the introduction as the server writes it, once forSite has rewritten the link', () => {
65
+ (0, chai_1.expect)(() => assertNoRawDocumentLink(guarded(makeSpec().info.description))).to.not.throw();
66
+ });
67
+ it('catches the link when the introduction is reworded', () => {
68
+ const text = guarded('Download the [raw document](/xenon/api-docs.json).');
69
+ (0, chai_1.expect)(() => assertNoRawDocumentLink(text)).to.throw(/still links to \/xenon\/api-docs\.json[\s\S]*src\/app\/swagger\.ts/);
70
+ });
71
+ it('catches a link that has a space, an angle bracket or a fragment in its target', () => {
72
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('a [b]( /xenon/api-docs.json)')).to.throw();
73
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('a [b](</xenon/api-docs.json>)')).to.throw();
74
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('a [b](/xenon/api-docs.json#info "title")')).to.throw();
75
+ });
76
+ it('lets plain code and other links through', () => {
77
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('at `/xenon/api-docs.json` on your server, see [the profile](/xenon/profile)')).to.not.throw();
78
+ });
79
+ });
80
+ });