@xenon-device-management/xenon 2.10.0 → 2.11.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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-LzfXiiZJ.js → AnnotationOverlay-BQPQ5bPF.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BzFDp4mK.js → ApiKeyGate-Bd7Pumwv.js} +1 -1
- package/lib/public/assets/{BugReportButton-BpgWvmZo.js → BugReportButton-iC7fxPxl.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-CCB-egSH.js → DeviceMosaicView-CNdES7nm.js} +1 -1
- package/lib/public/assets/{EmptyState-DkUVmANo.js → EmptyState-wZeJbGhN.js} +1 -1
- package/lib/public/assets/{FieldGroup-mbfnfTql.js → FieldGroup-tpRfmhlE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BH5usz4Y.js → FilterMenu-BZwOppId.js} +1 -1
- package/lib/public/assets/{Menu-Cbo9mdTk.js → Menu-CahoAx8L.js} +1 -1
- package/lib/public/assets/{Modal-D6XfQHPr.js → Modal-mcM5W_Ji.js} +1 -1
- package/lib/public/assets/{RecordingPage-CmFPkWqH.js → RecordingPage-B4dFBndn.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CSlKk2qC.js → RecordingsPage-BOez3f_9.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BFW9Dlqd.js → SegmentedControl-qMSiMN1d.js} +1 -1
- package/lib/public/assets/{SettingCard-C4DioTTi.js → SettingCard-rcPtJ7HG.js} +1 -1
- package/lib/public/assets/{Table-DrKjlL00.js → Table-J3uognu0.js} +1 -1
- package/lib/public/assets/{activity-C9bE7Jm8.js → activity-_5pNSfKT.js} +1 -1
- package/lib/public/assets/{ai-settings-DRYic4oO.js → ai-settings-DTPRbkei.js} +1 -1
- package/lib/public/assets/{api-keys-CaQU0g7c.js → api-keys-CxkIUo76.js} +1 -1
- package/lib/public/assets/apps-BQKfuN3J.css +1 -0
- package/lib/public/assets/{apps-c1gUFmmK.js → apps-Bv0pAfxz.js} +2 -2
- package/lib/public/assets/{arrow-left-DMvAWA8r.js → arrow-left-uY7F6z1K.js} +1 -1
- package/lib/public/assets/{arrow-right-BEQgxmKT.js → arrow-right-BbAm_2Wa.js} +1 -1
- package/lib/public/assets/{arrow-up-right-Ba0e2GeU.js → arrow-up-right-x4H5NuvE.js} +1 -1
- package/lib/public/assets/{auth-shell-COODQnc7.js → auth-shell-CtMFhbnC.js} +2 -2
- package/lib/public/assets/{builds-page-DZQgWBUs.js → builds-page-B_O5uBSZ.js} +1 -1
- package/lib/public/assets/{button-WW9j43wR.js → button-CQoMQEj_.js} +1 -1
- package/lib/public/assets/{calendar-COUs9VxF.js → calendar-BZkpCqlL.js} +1 -1
- package/lib/public/assets/{check-D8ifHIJR.js → check-CAB6RYuc.js} +1 -1
- package/lib/public/assets/{chevron-right-BjKVUlB_.js → chevron-right-ec0unu3u.js} +1 -1
- package/lib/public/assets/{circle-check-CfP79QE2.js → circle-check-Dfg-gpjg.js} +1 -1
- package/lib/public/assets/{circle-x-BZbFCixZ.js → circle-x-cbPq6Btp.js} +1 -1
- package/lib/public/assets/{clock-BuTm35Qg.js → clock-DU9cBLi8.js} +1 -1
- package/lib/public/assets/{copy-W00zUBwN.js → copy-VM9kWPpE.js} +1 -1
- package/lib/public/assets/{device-explorer-BEpS-Y-_.js → device-explorer-ai0usQ8m.js} +1 -1
- package/lib/public/assets/{download-CGz-MS3y.js → download-WgW4AVid.js} +1 -1
- package/lib/public/assets/{forgot-password-DG7SqVgU.js → forgot-password-BGRpjTtt.js} +1 -1
- package/lib/public/assets/{index-EkaQM1DK.js → index-C33GjwPr.js} +2 -2
- package/lib/public/assets/{index-DWHTp1YP.js → index-DNDdHt3h.js} +1 -1
- package/lib/public/assets/{input-ChaTXeCq.js → input-DwiVcO3Q.js} +1 -1
- package/lib/public/assets/{keycodes-BhbgeW_r.js → keycodes-CvCtpVJD.js} +1 -1
- package/lib/public/assets/{line-chart-9P3RxScy.js → line-chart-PE5e9PeN.js} +1 -1
- package/lib/public/assets/{list-checks-_23IIEEL.js → list-checks-Bgn9GEEO.js} +1 -1
- package/lib/public/assets/{lock-B2JaqqRI.js → lock-Cb7My6hc.js} +1 -1
- package/lib/public/assets/{login-BEWIfLfC.js → login-CDmyzSpl.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BGJIJQ-k.js → maintenance-settings-Bq6pKoBt.js} +1 -1
- package/lib/public/assets/{monitor-BrwMY4G0.js → monitor-BZSwrnPV.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DsWDMyrY.js → mouse-pointer-2-BbFj8y8i.js} +1 -1
- package/lib/public/assets/{network-DfIhNVJv.js → network-DpwYGB0-.js} +1 -1
- package/lib/public/assets/{overview-B1mePY2_.js → overview-B4_IO-X8.js} +1 -1
- package/lib/public/assets/{page-header-DoD3TOzZ.js → page-header-BpLEuo7i.js} +1 -1
- package/lib/public/assets/{plus-BGvWvpBB.js → plus-DHZS0EGU.js} +1 -1
- package/lib/public/assets/{profile-page-CvV5aWvl.js → profile-page-DsHDIq-Y.js} +1 -1
- package/lib/public/assets/{reset-password-CGVd31LR.js → reset-password-DuUjhw_l.js} +1 -1
- package/lib/public/assets/{runbook-page-Bq2cvwb5.js → runbook-page-BI_ayvLd.js} +1 -1
- package/lib/public/assets/{select-CTcD4jWR.js → select-xiba9PSx.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-Emz2obK_.js +1 -0
- package/lib/public/assets/selector-health-page-zpwrgalC.js +11 -0
- package/lib/public/assets/session-detail-page-BhlTJ1Oq.js +4 -0
- package/lib/public/assets/{settings-C-EnGIqs.js → settings-B-mabJFm.js} +1 -1
- package/lib/public/assets/{stat-tile-BY0RDTRm.js → stat-tile-lT8WcPgu.js} +1 -1
- package/lib/public/assets/{tablet-B4cFM-5B.js → tablet-DM8dp75P.js} +1 -1
- package/lib/public/assets/{teams-BFe-RpYd.js → teams-CRkb5-Fe.js} +1 -1
- package/lib/public/assets/{trash-2-CJCKBOP9.js → trash-2-Dxgryn4o.js} +1 -1
- package/lib/public/assets/{upload-DidWkbld.js → upload-Ci0vz0BP.js} +1 -1
- package/lib/public/assets/{use-builds-data-fnfxatOd.js → use-builds-data-DaaqMjjL.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-iPen3tmC.js → use-password-reset-mode-BvIyZtaF.js} +1 -1
- package/lib/public/assets/{users-CrcjmRdf.js → users-BT8qx3ea.js} +1 -1
- package/lib/public/assets/{users-DBUv0CJx.js → users-CWYHk75q.js} +1 -1
- package/lib/public/assets/{video-off-eh69-FKB.js → video-off-BvY08g6I.js} +1 -1
- package/lib/public/assets/view-state-83Z1gFME.js +1 -0
- package/lib/public/assets/{webhook-settings-CQoSshBn.js → webhook-settings-aHIlSJFP.js} +1 -1
- package/lib/public/assets/{zap-CICtWiLn.js → zap-qG2Li6rI.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/index.js +2 -0
- package/lib/src/dashboard/event-manager.js +4 -6
- package/lib/src/gateway/nodeSessionMetrics.js +39 -0
- package/lib/src/gateway/nodeSessionStatus.js +32 -23
- package/lib/src/generated/client/edge.js +2 -2
- package/lib/src/generated/client/index.js +2 -2
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +6 -0
- package/lib/src/plugin.js +3 -0
- package/lib/src/services/SelectorVerificationJob.js +50 -5
- package/lib/src/services/SessionLifecycleService.js +16 -0
- package/lib/src/services/metrics/NodeMetricsCollector.js +114 -0
- package/lib/src/services/metrics/NodeMetricsStore.js +76 -0
- package/lib/src/services/metrics/SessionMetricsService.js +71 -14
- package/lib/src/services/metrics/nodeMetrics.js +92 -0
- package/lib/src/services/selector-health/selectorDetail.js +36 -20
- package/lib/src/sessions/RemoteSession.js +32 -0
- package/lib/src/sessions/SessionManager.js +63 -0
- package/lib/test/helpers/scratch-database.js +17 -3
- package/lib/test/unit/dashboard-events-metrics.spec.js +10 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +330 -0
- package/lib/test/unit/node-metrics-collector.spec.js +226 -0
- package/lib/test/unit/node-metrics-store.spec.js +70 -0
- package/lib/test/unit/node-session-metrics-route.spec.js +97 -0
- package/lib/test/unit/selector-health-detail.spec.js +57 -0
- package/lib/test/unit/selector-verification-job.spec.js +3 -0
- package/lib/test/unit/selector-verification-missed-regression.spec.js +132 -0
- package/lib/test/unit/session-log-indexes.spec.js +147 -0
- package/lib/test/unit/session-metrics-lifecycle.spec.js +55 -0
- package/lib/test/unit/session-metrics-service.spec.js +97 -0
- package/lib/test/unit/session-recovery-metrics.spec.js +94 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002180000_session_log_indexes/migration.sql +5 -0
- package/prisma/migrations/20261002190000_log_profiling_session_indexes/migration.sql +5 -0
- package/prisma/schema.prisma +6 -0
- package/lib/public/assets/apps-DMhpZuKd.css +0 -1
- package/lib/public/assets/selector-detail-redirect-19odlD4B.js +0 -1
- package/lib/public/assets/selector-health-page-LqIsLrjW.js +0 -11
- package/lib/public/assets/session-detail-page-DQzb8_fq.js +0 -4
- package/lib/public/assets/view-state-CP9dE1UI.js +0 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const NodeMetricsCollector_1 = require("../../src/services/metrics/NodeMetricsCollector");
|
|
19
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
20
|
+
const sample = (at) => ({
|
|
21
|
+
at,
|
|
22
|
+
deviceCpuPct: 5,
|
|
23
|
+
deviceMemMb: 1,
|
|
24
|
+
deviceMemTotalMb: 2,
|
|
25
|
+
appCpuPct: null,
|
|
26
|
+
appMemMb: null,
|
|
27
|
+
appId: null,
|
|
28
|
+
});
|
|
29
|
+
const answer = (state, ats) => ({
|
|
30
|
+
kind: 'answer',
|
|
31
|
+
answer: { platform: 'android', state, samples: ats.map(sample) },
|
|
32
|
+
});
|
|
33
|
+
describe('NodeMetricsCollector: the hub collects a node session’s figures', () => {
|
|
34
|
+
let clock;
|
|
35
|
+
let replies;
|
|
36
|
+
let asked;
|
|
37
|
+
let got;
|
|
38
|
+
let warned;
|
|
39
|
+
let support;
|
|
40
|
+
/** While set, an ask waits on it: a node slow to answer. */
|
|
41
|
+
let gate;
|
|
42
|
+
const source = {
|
|
43
|
+
nodeOrigin: () => 'http://node:4723',
|
|
44
|
+
nodeMetrics: (after) => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
var _a;
|
|
46
|
+
asked.push(after);
|
|
47
|
+
if (gate)
|
|
48
|
+
yield gate;
|
|
49
|
+
return (_a = replies.shift()) !== null && _a !== void 0 ? _a : answer('sampling', []);
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
const make = (after = null) => new NodeMetricsCollector_1.NodeMetricsCollector({
|
|
53
|
+
source,
|
|
54
|
+
hooks: { onSample: (s) => got.push(s.at), onGiveUp: () => undefined },
|
|
55
|
+
support,
|
|
56
|
+
logger: { info: () => undefined, warn: (m) => warned.push(m) },
|
|
57
|
+
after,
|
|
58
|
+
});
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
clock = sinon_1.default.useFakeTimers({ now: 0 });
|
|
61
|
+
replies = [];
|
|
62
|
+
asked = [];
|
|
63
|
+
got = [];
|
|
64
|
+
warned = [];
|
|
65
|
+
gate = null;
|
|
66
|
+
support = new nodeMetrics_1.NodeMetricsSupport();
|
|
67
|
+
support.now = () => clock.now;
|
|
68
|
+
support.logger = { warn: (m) => warned.push(m) };
|
|
69
|
+
});
|
|
70
|
+
afterEach(() => clock.restore());
|
|
71
|
+
it('asks every 10 s from the newest sample it has, and passes the samples on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
replies = [answer('sampling', [1, 2]), answer('sampling', [3])];
|
|
73
|
+
const c = make();
|
|
74
|
+
c.start();
|
|
75
|
+
yield clock.tickAsync(10000);
|
|
76
|
+
yield clock.tickAsync(10000);
|
|
77
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
78
|
+
(0, chai_1.expect)(got).to.deep.equal([1, 2, 3]);
|
|
79
|
+
(0, chai_1.expect)(c.state()).to.equal('sampling');
|
|
80
|
+
}));
|
|
81
|
+
it('starts from the newest sample stored, after a hub restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
const c = make(500);
|
|
83
|
+
c.start();
|
|
84
|
+
yield clock.tickAsync(10000);
|
|
85
|
+
(0, chai_1.expect)(asked).to.deep.equal([500]);
|
|
86
|
+
}));
|
|
87
|
+
it('asks once more when the session ends, and takes the last samples', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
replies = [answer('sampling', [1]), answer('ended', [2, 3])];
|
|
89
|
+
const c = make();
|
|
90
|
+
c.start();
|
|
91
|
+
yield clock.tickAsync(10000);
|
|
92
|
+
yield c.stop();
|
|
93
|
+
(0, chai_1.expect)(got).to.deep.equal([1, 2, 3]);
|
|
94
|
+
(0, chai_1.expect)(c.state()).to.equal('stopped');
|
|
95
|
+
yield clock.tickAsync(30000);
|
|
96
|
+
(0, chai_1.expect)(asked).to.have.length(2);
|
|
97
|
+
}));
|
|
98
|
+
it('stops asking when the node says off, or that its sampler stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
for (const [state, ats, expected] of [
|
|
100
|
+
['off', [], 'off'],
|
|
101
|
+
['stopped', [7], 'stopped'],
|
|
102
|
+
]) {
|
|
103
|
+
asked = [];
|
|
104
|
+
replies = [answer(state, [...ats])];
|
|
105
|
+
const c = make();
|
|
106
|
+
c.start();
|
|
107
|
+
yield clock.tickAsync(30000);
|
|
108
|
+
(0, chai_1.expect)(asked, state).to.have.length(1);
|
|
109
|
+
(0, chai_1.expect)(c.state(), state).to.equal(expected);
|
|
110
|
+
yield c.stop();
|
|
111
|
+
(0, chai_1.expect)(asked, `${state}: no last ask`).to.have.length(1);
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
it('stops asking a node without the route, says so once, and asks it again later', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
replies = [{ kind: 'unsupported', status: 404 }];
|
|
116
|
+
const first = make();
|
|
117
|
+
first.start();
|
|
118
|
+
yield clock.tickAsync(10000);
|
|
119
|
+
(0, chai_1.expect)(first.state()).to.equal('off');
|
|
120
|
+
const second = make();
|
|
121
|
+
second.start();
|
|
122
|
+
yield clock.tickAsync(10000);
|
|
123
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
124
|
+
(0, chai_1.expect)(second.state()).to.equal('off');
|
|
125
|
+
(0, chai_1.expect)(warned.filter((m) => m.includes('http://node:4723'))).to.have.length(1);
|
|
126
|
+
yield clock.tickAsync(10 * 60000);
|
|
127
|
+
const third = make();
|
|
128
|
+
third.start();
|
|
129
|
+
yield clock.tickAsync(10000);
|
|
130
|
+
(0, chai_1.expect)(asked).to.have.length(2);
|
|
131
|
+
}));
|
|
132
|
+
it('keeps the figures it has when the node later says off or refuses: it says stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
+
for (const last of [answer('off', []), { kind: 'refused' }]) {
|
|
134
|
+
asked = [];
|
|
135
|
+
replies = [answer('sampling', [1]), last];
|
|
136
|
+
const c = make();
|
|
137
|
+
c.start();
|
|
138
|
+
yield clock.tickAsync(20000);
|
|
139
|
+
(0, chai_1.expect)(asked, last.kind).to.have.length(2);
|
|
140
|
+
(0, chai_1.expect)(c.state(), last.kind).to.equal('stopped');
|
|
141
|
+
}
|
|
142
|
+
}));
|
|
143
|
+
it("stops asking when the node refuses the hub's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
144
|
+
replies = [{ kind: 'refused' }];
|
|
145
|
+
const c = make();
|
|
146
|
+
c.start();
|
|
147
|
+
yield clock.tickAsync(30000);
|
|
148
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
149
|
+
(0, chai_1.expect)(c.state()).to.equal('off');
|
|
150
|
+
}));
|
|
151
|
+
it('keeps asking an unreachable node, saying so once per outage', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
152
|
+
replies = [
|
|
153
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
154
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
155
|
+
answer('sampling', [1]),
|
|
156
|
+
{ kind: 'unavailable', reason: 'timeout' },
|
|
157
|
+
];
|
|
158
|
+
const c = make();
|
|
159
|
+
c.start();
|
|
160
|
+
yield clock.tickAsync(40000);
|
|
161
|
+
(0, chai_1.expect)(asked).to.have.length(4);
|
|
162
|
+
(0, chai_1.expect)(got).to.deep.equal([1]);
|
|
163
|
+
(0, chai_1.expect)(warned).to.have.length(2);
|
|
164
|
+
(0, chai_1.expect)(c.state()).to.equal('sampling');
|
|
165
|
+
}));
|
|
166
|
+
it('finishes stopping when the node is unreachable', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
167
|
+
replies = [{ kind: 'unavailable', reason: 'timeout' }];
|
|
168
|
+
const c = make();
|
|
169
|
+
c.start();
|
|
170
|
+
yield c.stop();
|
|
171
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
172
|
+
}));
|
|
173
|
+
it('never asks twice at once, or takes a sample twice', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
174
|
+
let release;
|
|
175
|
+
gate = new Promise((r) => (release = r));
|
|
176
|
+
replies = [answer('sampling', [1, 2]), answer('sampling', [2, 3])];
|
|
177
|
+
const c = make();
|
|
178
|
+
c.start();
|
|
179
|
+
yield clock.tickAsync(10000); // the first ask, held by the node
|
|
180
|
+
yield clock.tickAsync(20000); // two more ticks while it is held
|
|
181
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
182
|
+
gate = null;
|
|
183
|
+
release();
|
|
184
|
+
yield clock.tickAsync(10000); // the first answer arrives; the next ask goes
|
|
185
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
186
|
+
(0, chai_1.expect)(got).to.deep.equal([1, 2, 3]);
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
189
|
+
describe("readNodeMetricsReply: what a node's answer means", () => {
|
|
190
|
+
const h = { [nodeMetrics_1.NODE_METRICS_HEADER]: '1' };
|
|
191
|
+
it('reads each answer', () => {
|
|
192
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(404, {}, undefined)).to.deep.equal({
|
|
193
|
+
kind: 'unsupported',
|
|
194
|
+
status: 404,
|
|
195
|
+
});
|
|
196
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(404, h, {})).to.deep.equal({ kind: 'refused' });
|
|
197
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(200, h, {
|
|
198
|
+
value: { platform: 'ios', state: 'sampling', samples: [sample(1)] },
|
|
199
|
+
})).to.deep.equal({
|
|
200
|
+
kind: 'answer',
|
|
201
|
+
answer: { platform: 'ios', state: 'sampling', samples: [sample(1)] },
|
|
202
|
+
});
|
|
203
|
+
// Without the header: an older node only for what an older node answers
|
|
204
|
+
// (its login's 401, an unknown route's 404, a catch-all's 2xx). A proxy's
|
|
205
|
+
// 502 or 503 in front of a new node is an outage, not an older node.
|
|
206
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(401, {}, {})).to.deep.equal({ kind: 'unsupported', status: 401 });
|
|
207
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(200, {}, '<html>')).to.deep.equal({
|
|
208
|
+
kind: 'unsupported',
|
|
209
|
+
status: 200,
|
|
210
|
+
});
|
|
211
|
+
for (const status of [500, 502, 503, 504]) {
|
|
212
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(status, {}, 'Bad Gateway'), String(status)).to.deep.equal({
|
|
213
|
+
kind: 'unavailable',
|
|
214
|
+
reason: `answered ${status}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(503, h, {})).to.deep.equal({
|
|
218
|
+
kind: 'unavailable',
|
|
219
|
+
reason: 'answered 503',
|
|
220
|
+
});
|
|
221
|
+
(0, chai_1.expect)((0, nodeMetrics_1.readNodeMetricsReply)(200, h, { value: { state: 'weird', samples: [] } })).to.deep.equal({
|
|
222
|
+
kind: 'unavailable',
|
|
223
|
+
reason: 'answered 200',
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("reflect-metadata");
|
|
4
|
+
const chai_1 = require("chai");
|
|
5
|
+
const NodeMetricsStore_1 = require("../../src/services/metrics/NodeMetricsStore");
|
|
6
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
7
|
+
const types_1 = require("../../src/services/metrics/types");
|
|
8
|
+
const sample = (at) => ({
|
|
9
|
+
at,
|
|
10
|
+
deviceCpuPct: 10,
|
|
11
|
+
deviceMemMb: 100,
|
|
12
|
+
deviceMemTotalMb: 4000,
|
|
13
|
+
appCpuPct: null,
|
|
14
|
+
appMemMb: null,
|
|
15
|
+
appId: null,
|
|
16
|
+
});
|
|
17
|
+
describe('NodeMetricsStore: a node holds its figures for the hub', () => {
|
|
18
|
+
let now;
|
|
19
|
+
let store;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
now = 1000000;
|
|
22
|
+
store = new NodeMetricsStore_1.NodeMetricsStore();
|
|
23
|
+
store.now = () => now;
|
|
24
|
+
});
|
|
25
|
+
it('answers every sample without `after`, and only newer ones with it', () => {
|
|
26
|
+
store.begin('s1', 'Android');
|
|
27
|
+
[1, 2, 3].forEach((t) => store.add('s1', sample(t)));
|
|
28
|
+
(0, chai_1.expect)(store.read('s1', null)).to.deep.equal({
|
|
29
|
+
platform: 'android',
|
|
30
|
+
state: 'sampling',
|
|
31
|
+
samples: [sample(1), sample(2), sample(3)],
|
|
32
|
+
});
|
|
33
|
+
(0, chai_1.expect)(store.read('s1', 2).samples.map((s) => s.at)).to.deep.equal([3]);
|
|
34
|
+
});
|
|
35
|
+
it('drops what the hub has collected', () => {
|
|
36
|
+
store.begin('s1', 'android');
|
|
37
|
+
[1, 2, 3].forEach((t) => store.add('s1', sample(t)));
|
|
38
|
+
store.read('s1', 2);
|
|
39
|
+
(0, chai_1.expect)(store.read('s1', null).samples.map((s) => s.at)).to.deep.equal([3]);
|
|
40
|
+
});
|
|
41
|
+
it('keeps the newest 900 samples', () => {
|
|
42
|
+
store.begin('s1', 'android');
|
|
43
|
+
for (let t = 1; t <= types_1.MAX_BUFFERED_SAMPLES + 5; t++)
|
|
44
|
+
store.add('s1', sample(t));
|
|
45
|
+
const ats = store.read('s1', null).samples.map((s) => s.at);
|
|
46
|
+
(0, chai_1.expect)(ats).to.have.length(types_1.MAX_BUFFERED_SAMPLES);
|
|
47
|
+
(0, chai_1.expect)(ats[0]).to.equal(6);
|
|
48
|
+
});
|
|
49
|
+
it('says a sampler that gave up stopped, and an ended session ended', () => {
|
|
50
|
+
store.begin('s1', 'android');
|
|
51
|
+
store.gaveUp('s1');
|
|
52
|
+
(0, chai_1.expect)(store.read('s1', null).state).to.equal('stopped');
|
|
53
|
+
store.end('s1');
|
|
54
|
+
(0, chai_1.expect)(store.read('s1', null).state).to.equal('ended');
|
|
55
|
+
});
|
|
56
|
+
it('keeps an ended session 10 minutes for the last collection, then forgets it', () => {
|
|
57
|
+
store.begin('s1', 'android');
|
|
58
|
+
store.add('s1', sample(1));
|
|
59
|
+
store.end('s1');
|
|
60
|
+
now += nodeMetrics_1.KEEP_AFTER_END_MS - 1;
|
|
61
|
+
(0, chai_1.expect)(store.read('s1', null).samples).to.have.length(1);
|
|
62
|
+
now += 1;
|
|
63
|
+
(0, chai_1.expect)(store.read('s1', null)).to.deep.equal({ platform: '', state: 'off', samples: [] });
|
|
64
|
+
});
|
|
65
|
+
it('says off for a session it has nothing for', () => {
|
|
66
|
+
(0, chai_1.expect)(store.read('nope', null)).to.deep.equal({ platform: '', state: 'off', samples: [] });
|
|
67
|
+
store.add('nope', sample(1));
|
|
68
|
+
(0, chai_1.expect)(store.read('nope', null).samples).to.deep.equal([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
19
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
20
|
+
const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
|
|
21
|
+
const nodeMetrics_1 = require("../../src/services/metrics/nodeMetrics");
|
|
22
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
23
|
+
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
24
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
25
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
26
|
+
const ANSWER = { platform: 'android', state: 'sampling', samples: [] };
|
|
27
|
+
describe("a node's session metrics route", () => {
|
|
28
|
+
let verify;
|
|
29
|
+
let read;
|
|
30
|
+
let enforced;
|
|
31
|
+
const app = () => {
|
|
32
|
+
const a = (0, express_1.default)();
|
|
33
|
+
a.get('/xenon/api/node/sessions/:sessionId/metrics', (0, nodeSessionMetrics_1.nodeSessionMetricsHandler)({
|
|
34
|
+
hubTokens: { verify },
|
|
35
|
+
enforced: () => enforced,
|
|
36
|
+
read,
|
|
37
|
+
logger: quiet,
|
|
38
|
+
}));
|
|
39
|
+
return a;
|
|
40
|
+
};
|
|
41
|
+
const get = (q = '') => (0, supertest_1.default)(app()).get(`/xenon/api/node/sessions/s1/metrics${q}`);
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
verify = sinon_1.default.stub();
|
|
44
|
+
read = sinon_1.default.stub().returns(ANSWER);
|
|
45
|
+
enforced = false;
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => sinon_1.default.restore());
|
|
48
|
+
it('answers the figures, with no credential when per-command auth is off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
const res = yield get('?after=1500');
|
|
50
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
51
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: ANSWER });
|
|
52
|
+
(0, chai_1.expect)(res.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
53
|
+
(0, chai_1.expect)(read.calledOnceWithExactly('s1', 1500)).to.equal(true);
|
|
54
|
+
}));
|
|
55
|
+
it("with per-command auth on, answers only the hub's token for that session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
enforced = true;
|
|
57
|
+
verify.withArgs('good', 's1').resolves(true);
|
|
58
|
+
verify.withArgs('other', 's1').resolves(false);
|
|
59
|
+
const missing = yield get();
|
|
60
|
+
const wrong = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'other');
|
|
61
|
+
const right = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
62
|
+
(0, chai_1.expect)([missing.status, missing.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
63
|
+
(0, chai_1.expect)([wrong.status, wrong.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
64
|
+
(0, chai_1.expect)(right.status).to.equal(200);
|
|
65
|
+
(0, chai_1.expect)(read.calledOnce).to.equal(true);
|
|
66
|
+
for (const r of [missing, wrong, right])
|
|
67
|
+
(0, chai_1.expect)(r.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
68
|
+
}));
|
|
69
|
+
it("answers 503 when it can't check the hub's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
enforced = true;
|
|
71
|
+
verify.rejects(new Error('JWKS unreachable'));
|
|
72
|
+
const res = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
73
|
+
(0, chai_1.expect)([res.status, res.body]).to.deep.equal([503, commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY]);
|
|
74
|
+
(0, chai_1.expect)(read.called).to.equal(false);
|
|
75
|
+
}));
|
|
76
|
+
it('reads anything but a time as no `after`', () => {
|
|
77
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)('1500')).to.equal(1500);
|
|
78
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)('1500.5')).to.equal(1500.5);
|
|
79
|
+
for (const bad of [undefined, '', 'abc', '-5', '1e3', ['1']]) {
|
|
80
|
+
(0, chai_1.expect)((0, nodeSessionMetrics_1.parseAfter)(bad), String(bad)).to.equal(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
it('exists only on a node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const mount = (hub) => {
|
|
85
|
+
const a = (0, express_1.default)();
|
|
86
|
+
const r = express_1.default.Router();
|
|
87
|
+
(0, nodeSessionMetrics_1.registerNodeSessionMetrics)(r, Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub }));
|
|
88
|
+
a.use('/xenon/api', r);
|
|
89
|
+
return a;
|
|
90
|
+
};
|
|
91
|
+
const onHub = yield (0, supertest_1.default)(mount()).get('/xenon/api/node/sessions/s1/metrics');
|
|
92
|
+
const onNode = yield (0, supertest_1.default)(mount('http://127.0.0.1:1')).get('/xenon/api/node/sessions/s1/metrics');
|
|
93
|
+
(0, chai_1.expect)(onHub.status).to.equal(404);
|
|
94
|
+
(0, chai_1.expect)(onHub.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal(undefined);
|
|
95
|
+
(0, chai_1.expect)(onNode.headers[nodeMetrics_1.NODE_METRICS_HEADER]).to.equal('1');
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const prisma_1 = require("../../src/prisma");
|
|
18
19
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
19
20
|
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
20
21
|
const selectorDetail_1 = require("../../src/services/selector-health/selectorDetail");
|
|
@@ -128,4 +129,60 @@ describe('GET /healing/selectors/detail (real queries)', function () {
|
|
|
128
129
|
const b = (yield detail(selector_health_fixture_1.READ_ONLY_A, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.hot })).body;
|
|
129
130
|
(0, chai_1.expect)(b.canAct).to.equal(false);
|
|
130
131
|
}));
|
|
132
|
+
it('reads each heal once without its session, and each session once', () => __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
// 45 heals of one selector: odd ones on the shared phone, even ones on
|
|
134
|
+
// team A's; two in three found the same fix.
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
yield scratch.db.sessionLog.createMany({
|
|
137
|
+
data: Array.from({ length: 45 }, (_, i) => ({
|
|
138
|
+
session_id: i % 2 ? 'sh-s-shared-1' : 'sh-s-a-1',
|
|
139
|
+
command_name: 'findElement',
|
|
140
|
+
url: '/element',
|
|
141
|
+
method: 'POST',
|
|
142
|
+
title: 'Find element',
|
|
143
|
+
response: '{}',
|
|
144
|
+
is_healed: true,
|
|
145
|
+
original_strategy: 'id',
|
|
146
|
+
original_selector: 'com.acme:id/many',
|
|
147
|
+
healed_strategy: 'id',
|
|
148
|
+
healed_selector: i % 3 ? 'com.acme:id/many_v2' : 'com.acme:id/many_v3',
|
|
149
|
+
healing_tier: i % 2 ? 'LLM' : 'Fuzzy XML',
|
|
150
|
+
healing_confidence: i % 3 ? 0.6 : null,
|
|
151
|
+
duration: 100,
|
|
152
|
+
createdAt: new Date(now - (i + 1) * selector_health_fixture_1.HOUR),
|
|
153
|
+
})),
|
|
154
|
+
});
|
|
155
|
+
const res = yield detail(selector_health_fixture_1.ADMIN, { strategy: 'id', selector: 'com.acme:id/many', days: '30' });
|
|
156
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
157
|
+
// Attaching the session to each heal took most of a second for a selector
|
|
158
|
+
// healed 50,000 times; the heals' sessions are read once each instead.
|
|
159
|
+
const heals = prisma_1.prisma.sessionLog.findMany.args.map((a) => a[0]);
|
|
160
|
+
(0, chai_1.expect)(heals).to.have.length(1);
|
|
161
|
+
(0, chai_1.expect)(heals[0].select).to.not.have.property('session');
|
|
162
|
+
const sessions = yield Promise.all(prisma_1.prisma.session.findMany.returnValues);
|
|
163
|
+
(0, chai_1.expect)(sessions
|
|
164
|
+
.flat()
|
|
165
|
+
.map((x) => x.id)
|
|
166
|
+
.sort()).to.deep.equal(['sh-s-a-1', 'sh-s-shared-1']);
|
|
167
|
+
const b = res.body;
|
|
168
|
+
(0, chai_1.expect)(b).to.deep.include({ heals: 45, sessions: 2, timeSpentMs: 4500 });
|
|
169
|
+
(0, chai_1.expect)(b.firstHealedAt).to.equal(new Date(now - 45 * selector_health_fixture_1.HOUR).toISOString());
|
|
170
|
+
(0, chai_1.expect)(b.lastHealedAt).to.equal(new Date(now - selector_health_fixture_1.HOUR).toISOString());
|
|
171
|
+
(0, chai_1.expect)(b.daily.reduce((s, d) => s + d.heals, 0)).to.equal(45);
|
|
172
|
+
(0, chai_1.expect)(b.recent).to.have.length(20);
|
|
173
|
+
(0, chai_1.expect)(b.recent[0].at).to.equal(new Date(now - selector_health_fixture_1.HOUR).toISOString());
|
|
174
|
+
(0, chai_1.expect)(b.suggestions.map((x) => [x.selector, x.count])).to.deep.equal([
|
|
175
|
+
['com.acme:id/many_v2', 30],
|
|
176
|
+
['com.acme:id/many_v3', 15],
|
|
177
|
+
]);
|
|
178
|
+
(0, chai_1.expect)(b.suggestions[0].methods).to.have.members(['LLM', 'Fuzzy XML']);
|
|
179
|
+
(0, chai_1.expect)(b.suggestions[0].averageConfidence).to.be.closeTo(0.6, 1e-9);
|
|
180
|
+
(0, chai_1.expect)(b.suggestions[1].averageConfidence).to.equal(null);
|
|
181
|
+
(0, chai_1.expect)(b.platforms).to.deep.equal([{ name: 'android', count: 45 }]);
|
|
182
|
+
(0, chai_1.expect)(b.builds).to.deep.equal([{ id: selector_health_fixture_1.BUILD.id, name: selector_health_fixture_1.BUILD.name, count: 45 }]);
|
|
183
|
+
(0, chai_1.expect)(b.devices).to.deep.equal([
|
|
184
|
+
{ udid: selector_health_fixture_1.PHONE.a, name: 'Team A Galaxy', count: 23 },
|
|
185
|
+
{ udid: selector_health_fixture_1.PHONE.shared, name: 'Shared Pixel', count: 22 },
|
|
186
|
+
]);
|
|
187
|
+
}));
|
|
131
188
|
});
|
|
@@ -41,6 +41,9 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
41
41
|
update: sinon_1.default.stub().resolves({}),
|
|
42
42
|
},
|
|
43
43
|
selectorEvent: { create: sinon_1.default.stub().resolves({}) },
|
|
44
|
+
// No heal since any selector was marked fixed: see
|
|
45
|
+
// selector-verification-missed-regression.spec.ts for one.
|
|
46
|
+
sessionLog: { findFirst: sinon_1.default.stub().resolves(null) },
|
|
44
47
|
$queryRaw: sinon_1.default.stub(),
|
|
45
48
|
};
|
|
46
49
|
prismaStub.$transaction = sinon_1.default
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
19
|
+
const SelectorVerificationJob_1 = require("../../src/services/SelectorVerificationJob");
|
|
20
|
+
const HOUR = 60 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* A heal of a selector someone marked fixed turns it back to "To fix" at once
|
|
23
|
+
* (SelectorStateService.onHealRecorded, called after the heal is written). That
|
|
24
|
+
* call can fail, say on a busy database, and nothing tried again: the selector
|
|
25
|
+
* stayed "being verified", and was even promoted to fixed once three other
|
|
26
|
+
* builds ran clean. The verification job now catches such a heal.
|
|
27
|
+
*/
|
|
28
|
+
describe('SelectorVerificationJob catches a heal the live check missed', function () {
|
|
29
|
+
this.timeout(90000);
|
|
30
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
31
|
+
let socket;
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const fixedAt = new Date(now - 48 * HOUR);
|
|
34
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
for (const b of ['mr-b1', 'mr-b2', 'mr-b3', 'mr-b4']) {
|
|
36
|
+
yield scratch.db.build.create({ data: { id: b, name: b } });
|
|
37
|
+
yield scratch.db.session.create({
|
|
38
|
+
data: {
|
|
39
|
+
id: `mr-s-${b}`,
|
|
40
|
+
build_id: b,
|
|
41
|
+
device_udid: 'mr-phone',
|
|
42
|
+
device_platform: 'android',
|
|
43
|
+
device_version: '14',
|
|
44
|
+
desired_capabilities: '{}',
|
|
45
|
+
session_capabilities: '{}',
|
|
46
|
+
node_id: 'localhost',
|
|
47
|
+
has_live_video: false,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
yield scratch.db.selectorEvent.deleteMany();
|
|
54
|
+
yield scratch.db.selectorState.deleteMany();
|
|
55
|
+
yield scratch.db.sessionLog.deleteMany();
|
|
56
|
+
socket = { emitToDashboard: sinon_1.default.stub() };
|
|
57
|
+
}));
|
|
58
|
+
const find = (build, selector, healed, at) => scratch.db.sessionLog.create({
|
|
59
|
+
data: {
|
|
60
|
+
session_id: `mr-s-${build}`,
|
|
61
|
+
command_name: 'findElement',
|
|
62
|
+
url: '/element',
|
|
63
|
+
method: 'POST',
|
|
64
|
+
title: 'Find element',
|
|
65
|
+
response: '{}',
|
|
66
|
+
is_healed: healed,
|
|
67
|
+
original_strategy: 'xpath',
|
|
68
|
+
original_selector: selector,
|
|
69
|
+
createdAt: new Date(at),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const state = (selector, status) => scratch.db.selectorState.create({
|
|
73
|
+
data: {
|
|
74
|
+
original_strategy: 'xpath',
|
|
75
|
+
original_selector: selector,
|
|
76
|
+
status,
|
|
77
|
+
fixed_at: fixedAt,
|
|
78
|
+
resolved_at: status === 'resolved' ? new Date(now - 24 * HOUR) : null,
|
|
79
|
+
clean_builds_count: status === 'resolved' ? 3 : 0,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const run = () => new SelectorVerificationJob_1.SelectorVerificationJob(scratch.db, socket).run();
|
|
83
|
+
const rowOf = (selector) => scratch.db.selectorState.findUniqueOrThrow({
|
|
84
|
+
where: {
|
|
85
|
+
original_strategy_original_selector: {
|
|
86
|
+
original_strategy: 'xpath',
|
|
87
|
+
original_selector: selector,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const actionsOf = (selector) => __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
return (yield scratch.db.selectorEvent.findMany({
|
|
93
|
+
where: { original_selector: selector },
|
|
94
|
+
orderBy: { createdAt: 'asc' },
|
|
95
|
+
})).map((e) => e.action);
|
|
96
|
+
});
|
|
97
|
+
it('sends a selector being verified back to fix, rather than promoting it', () => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
yield state('//pending/broke', 'pending');
|
|
99
|
+
// Three clean builds since it was marked fixed, and one that healed.
|
|
100
|
+
for (const b of ['mr-b1', 'mr-b2', 'mr-b3'])
|
|
101
|
+
yield find(b, '//pending/broke', false, now - HOUR);
|
|
102
|
+
yield find('mr-b4', '//pending/broke', true, now - 2 * HOUR);
|
|
103
|
+
yield run();
|
|
104
|
+
const row = yield rowOf('//pending/broke');
|
|
105
|
+
(0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1, clean_builds_count: 0 });
|
|
106
|
+
(0, chai_1.expect)(row.fixed_at).to.equal(null);
|
|
107
|
+
(0, chai_1.expect)(yield actionsOf('//pending/broke')).to.deep.equal(['broke_again']);
|
|
108
|
+
(0, chai_1.expect)(socket.emitToDashboard.firstCall.args[0]).to.equal('selector_regressed');
|
|
109
|
+
}));
|
|
110
|
+
it('sends a fixed selector back to fix', () => __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
yield state('//resolved/broke', 'resolved');
|
|
112
|
+
yield find('mr-b1', '//resolved/broke', true, now - HOUR);
|
|
113
|
+
yield run();
|
|
114
|
+
const row = yield rowOf('//resolved/broke');
|
|
115
|
+
(0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1 });
|
|
116
|
+
(0, chai_1.expect)(row.resolved_at).to.equal(null);
|
|
117
|
+
(0, chai_1.expect)(yield actionsOf('//resolved/broke')).to.deep.equal(['broke_again']);
|
|
118
|
+
}));
|
|
119
|
+
it('leaves alone a selector that healed only before it was marked fixed', () => __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
yield state('//pending/clean', 'pending');
|
|
121
|
+
yield state('//resolved/clean', 'resolved');
|
|
122
|
+
yield find('mr-b1', '//pending/clean', true, fixedAt.getTime() - HOUR);
|
|
123
|
+
yield find('mr-b1', '//resolved/clean', true, fixedAt.getTime() - HOUR);
|
|
124
|
+
for (const b of ['mr-b1', 'mr-b2', 'mr-b3'])
|
|
125
|
+
yield find(b, '//pending/clean', false, now - HOUR);
|
|
126
|
+
yield run();
|
|
127
|
+
(0, chai_1.expect)((yield rowOf('//pending/clean')).status).to.equal('resolved');
|
|
128
|
+
(0, chai_1.expect)((yield rowOf('//resolved/clean')).status).to.equal('resolved');
|
|
129
|
+
(0, chai_1.expect)(yield actionsOf('//pending/clean')).to.deep.equal(['verified']);
|
|
130
|
+
(0, chai_1.expect)(yield actionsOf('//resolved/clean')).to.deep.equal([]);
|
|
131
|
+
}));
|
|
132
|
+
});
|