@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.
- package/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- 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
|
-
|
|
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
|
|
326
|
+
yield step.hold();
|
|
273
327
|
return { actorId: 'usr_alice' };
|
|
274
328
|
}),
|
|
275
329
|
});
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
|
357
|
+
yield step.hold();
|
|
309
358
|
return true;
|
|
310
359
|
}),
|
|
311
360
|
});
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
377
|
+
yield step.hold();
|
|
333
378
|
return { mux: gatedMux };
|
|
334
379
|
}),
|
|
335
380
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
});
|