@xenon-device-management/xenon 2.9.1 → 2.10.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 (148) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-Dm_URoOL.js → AnnotationOverlay-LzfXiiZJ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BdGOuXDm.js → ApiKeyGate-BzFDp4mK.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-BuWGEjgw.js → BugReportButton-BpgWvmZo.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-Pu3rJ99D.js → DeviceMosaicView-CCB-egSH.js} +2 -2
  6. package/lib/public/assets/{EmptyState-syyHElMd.js → EmptyState-DkUVmANo.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-BZR5ZBVv.js → FieldGroup-mbfnfTql.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-CsgtrmPb.js → FilterMenu-BH5usz4Y.js} +1 -1
  9. package/lib/public/assets/{Menu-DP8XOm1A.js → Menu-Cbo9mdTk.js} +1 -1
  10. package/lib/public/assets/{Modal-Dtd-8uI2.js → Modal-D6XfQHPr.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CdAtAmHj.js → RecordingPage-CmFPkWqH.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-Br3CKxTe.js → RecordingsPage-CSlKk2qC.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-BOaNmUL-.js → SegmentedControl-BFW9Dlqd.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CQuK9ZZX.js → SettingCard-C4DioTTi.js} +1 -1
  15. package/lib/public/assets/{Table-CswCefnT.js → Table-DrKjlL00.js} +1 -1
  16. package/lib/public/assets/{activity-6fhH3u4C.js → activity-C9bE7Jm8.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CU24L3D4.js → ai-settings-DRYic4oO.js} +1 -1
  18. package/lib/public/assets/{api-keys-DC3BZDaG.js → api-keys-CaQU0g7c.js} +1 -1
  19. package/lib/public/assets/{apps-Ce5Z-wNc.js → apps-c1gUFmmK.js} +1 -1
  20. package/lib/public/assets/{arrow-left-DZJqYn7r.js → arrow-left-DMvAWA8r.js} +1 -1
  21. package/lib/public/assets/{arrow-right-nIvK8CyN.js → arrow-right-BEQgxmKT.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BfugJj3f.js → arrow-up-right-Ba0e2GeU.js} +1 -1
  23. package/lib/public/assets/{auth-shell-B1Cz75Vq.js → auth-shell-COODQnc7.js} +2 -2
  24. package/lib/public/assets/{builds-page-5mlJGjIk.js → builds-page-DZQgWBUs.js} +4 -4
  25. package/lib/public/assets/{button-D_W88sn-.js → button-WW9j43wR.js} +1 -1
  26. package/lib/public/assets/{calendar-Dc6pWZS9.js → calendar-COUs9VxF.js} +1 -1
  27. package/lib/public/assets/{check-BbvtnP8m.js → check-D8ifHIJR.js} +1 -1
  28. package/lib/public/assets/{chevron-right-CgBUaRCA.js → chevron-right-BjKVUlB_.js} +1 -1
  29. package/lib/public/assets/{circle-check-BWjEKvA4.js → circle-check-CfP79QE2.js} +1 -1
  30. package/lib/public/assets/{circle-x-BLUdeRhT.js → circle-x-BZbFCixZ.js} +1 -1
  31. package/lib/public/assets/{clock-PAhKG1jT.js → clock-BuTm35Qg.js} +1 -1
  32. package/lib/public/assets/{copy-2CHdAPBf.js → copy-W00zUBwN.js} +1 -1
  33. package/lib/public/assets/{device-explorer-EyAhEL_S.js → device-explorer-BEpS-Y-_.js} +5 -5
  34. package/lib/public/assets/{download-B1vKGhGP.js → download-CGz-MS3y.js} +1 -1
  35. package/lib/public/assets/{forgot-password-NSrrBA5N.js → forgot-password-DG7SqVgU.js} +1 -1
  36. package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
  37. package/lib/public/assets/{index-BEk1D5-B.js → index-DWHTp1YP.js} +1 -1
  38. package/lib/public/assets/{index-BxaH84UO.js → index-EkaQM1DK.js} +2 -2
  39. package/lib/public/assets/{input-cA_lrNIw.js → input-ChaTXeCq.js} +1 -1
  40. package/lib/public/assets/{keycodes-DZLCZ-uv.js → keycodes-BhbgeW_r.js} +10 -5
  41. package/lib/public/assets/line-chart-9P3RxScy.js +1 -0
  42. package/lib/public/assets/list-checks-_23IIEEL.js +11 -0
  43. package/lib/public/assets/{lock-ChoclF8X.js → lock-B2JaqqRI.js} +1 -1
  44. package/lib/public/assets/{login-63DiXJ0M.js → login-BEWIfLfC.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-A47Y-PYy.js → maintenance-settings-BGJIJQ-k.js} +1 -1
  46. package/lib/public/assets/{monitor-BYPF43Ds.js → monitor-BrwMY4G0.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-Pcl7inmZ.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
  48. package/lib/public/assets/{network-ChaLfnWe.js → network-DfIhNVJv.js} +1 -1
  49. package/lib/public/assets/overview-B1mePY2_.js +6 -0
  50. package/lib/public/assets/{page-header-bv2AQSMG.js → page-header-DoD3TOzZ.js} +1 -1
  51. package/lib/public/assets/{plus-u0x2Y5uW.js → plus-BGvWvpBB.js} +1 -1
  52. package/lib/public/assets/{profile-page-Cvmef2iY.js → profile-page-CvV5aWvl.js} +1 -1
  53. package/lib/public/assets/{reset-password-DqStVhBo.js → reset-password-CGVd31LR.js} +1 -1
  54. package/lib/public/assets/{runbook-page-UK0C95QF.js → runbook-page-Bq2cvwb5.js} +1 -1
  55. package/lib/public/assets/{select-CQ6O804R.js → select-CTcD4jWR.js} +1 -1
  56. package/lib/public/assets/selector-detail-redirect-19odlD4B.js +1 -0
  57. package/lib/public/assets/selector-health-page-LqIsLrjW.js +11 -0
  58. package/lib/public/assets/session-detail-page-DQzb8_fq.js +4 -0
  59. package/lib/public/assets/{settings-Cva7V4n-.js → settings-C-EnGIqs.js} +1 -1
  60. package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
  61. package/lib/public/assets/{tablet-BPtlTsKn.js → tablet-B4cFM-5B.js} +1 -1
  62. package/lib/public/assets/{teams-D4r1UMME.js → teams-BFe-RpYd.js} +1 -1
  63. package/lib/public/assets/time-D-gjmE1o.js +1 -0
  64. package/lib/public/assets/{trash-2-CnysPtdR.js → trash-2-CJCKBOP9.js} +1 -1
  65. package/lib/public/assets/{upload-Uyj4-Cyq.js → upload-DidWkbld.js} +1 -1
  66. package/lib/public/assets/use-builds-data-fnfxatOd.js +8 -0
  67. package/lib/public/assets/{use-password-reset-mode-Du-ykGHP.js → use-password-reset-mode-iPen3tmC.js} +1 -1
  68. package/lib/public/assets/{users-ttkwKBos.js → users-CrcjmRdf.js} +1 -1
  69. package/lib/public/assets/{users-Cs00GNmy.js → users-DBUv0CJx.js} +1 -1
  70. package/lib/public/assets/{video-off-BYa4qD8N.js → video-off-eh69-FKB.js} +1 -1
  71. package/lib/public/assets/view-state-CP9dE1UI.js +1 -0
  72. package/lib/public/assets/{webhook-settings-DuEnPlet.js → webhook-settings-CQoSshBn.js} +1 -1
  73. package/lib/public/assets/{zap-CxHn6Vwu.js → zap-CICtWiLn.js} +1 -1
  74. package/lib/public/index.html +2 -2
  75. package/lib/schema.json +5 -0
  76. package/lib/src/app/routers/dashboard.js +86 -40
  77. package/lib/src/app/routers/selector-health.js +75 -0
  78. package/lib/src/app/swagger-docs.js +77 -8
  79. package/lib/src/dashboard/event-manager.js +15 -99
  80. package/lib/src/device-managers/ios/IOSDiscoveryService.js +6 -65
  81. package/lib/src/device-managers/ios/IOSStreamService.js +7 -66
  82. package/lib/src/device-managers/ios/WDAClient.js +65 -86
  83. package/lib/src/generated/client/edge.js +27 -3
  84. package/lib/src/generated/client/index-browser.js +24 -0
  85. package/lib/src/generated/client/index.d.ts +8207 -5283
  86. package/lib/src/generated/client/index.js +27 -3
  87. package/lib/src/generated/client/package.json +1 -1
  88. package/lib/src/generated/client/schema.prisma +43 -9
  89. package/lib/src/generated/client/wasm.js +24 -0
  90. package/lib/src/prisma.js +2 -0
  91. package/lib/src/services/CleanupService.js +1 -0
  92. package/lib/src/services/NotificationService.js +3 -4
  93. package/lib/src/services/SelectorStateService.js +151 -113
  94. package/lib/src/services/SelectorVerificationJob.js +14 -9
  95. package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
  96. package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
  97. package/lib/src/services/metrics/SessionMetricsService.js +238 -0
  98. package/lib/src/services/metrics/androidMetrics.js +94 -0
  99. package/lib/src/services/metrics/iosMetrics.js +26 -0
  100. package/lib/src/services/metrics/metricsBody.js +37 -0
  101. package/lib/src/services/metrics/types.js +19 -0
  102. package/lib/src/services/selector-health/access.js +66 -0
  103. package/lib/src/services/selector-health/healingTrend.js +49 -0
  104. package/lib/src/services/selector-health/people.js +27 -0
  105. package/lib/src/services/selector-health/selectorDetail.js +198 -0
  106. package/lib/src/services/selector-health/selectorKeys.js +36 -0
  107. package/lib/src/services/selector-health/selectorList.js +333 -0
  108. package/lib/src/services/selector-health/stateView.js +67 -0
  109. package/lib/test/helpers/scratch-database.js +2 -0
  110. package/lib/test/helpers/selector-health-fixture.js +216 -0
  111. package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
  112. package/lib/test/unit/CleanupService.spec.js +28 -2
  113. package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
  114. package/lib/test/unit/healing-no-cost.spec.js +134 -0
  115. package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
  116. package/lib/test/unit/healing-trend.spec.js +118 -0
  117. package/lib/test/unit/ios-discovery-same-host.spec.js +0 -1
  118. package/lib/test/unit/ios-no-network-fallback.spec.js +219 -0
  119. package/lib/test/unit/selector-event-model.spec.js +47 -0
  120. package/lib/test/unit/selector-health-actions.spec.js +106 -0
  121. package/lib/test/unit/selector-health-detail.spec.js +131 -0
  122. package/lib/test/unit/selector-health-list.spec.js +185 -0
  123. package/lib/test/unit/selector-state-service.spec.js +74 -1
  124. package/lib/test/unit/selector-verification-job.spec.js +38 -0
  125. package/lib/test/unit/session-metrics-parse.spec.js +138 -0
  126. package/lib/test/unit/session-metrics-route.spec.js +140 -0
  127. package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
  128. package/lib/test/unit/session-metrics-service.spec.js +215 -0
  129. package/lib/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +1 -1
  131. package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
  132. package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
  133. package/prisma/schema.prisma +43 -9
  134. package/schema.json +5 -0
  135. package/lib/public/assets/chevron-left-DQDpR9ne.js +0 -6
  136. package/lib/public/assets/funnel-C9mzkCSI.js +0 -6
  137. package/lib/public/assets/layers-BuXMIJz3.js +0 -6
  138. package/lib/public/assets/list-checks-B8q68tPG.js +0 -6
  139. package/lib/public/assets/overview-bj_vktQG.js +0 -1
  140. package/lib/public/assets/selector-detail-page-D-PjU6mC.js +0 -1
  141. package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
  142. package/lib/public/assets/selector-health-CVm0cTOU.js +0 -16
  143. package/lib/public/assets/selector-health-page-BdibBs8g.js +0 -16
  144. package/lib/public/assets/session-detail-page-BijicGOF.js +0 -4
  145. package/lib/public/assets/stat-tile-BdIoEoou.js +0 -8
  146. package/lib/public/assets/time-B8FIgwPE.js +0 -1
  147. package/lib/src/profiling/AndroidAppProfiler.js +0 -213
  148. /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const supertest_1 = __importDefault(require("supertest"));
18
+ const scratch_database_1 = require("../helpers/scratch-database");
19
+ const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
20
+ const selectorList_1 = require("../../src/services/selector-health/selectorList");
21
+ describe('selector list: the query', () => {
22
+ it('reads every field, and falls back for anything it does not know', () => {
23
+ (0, chai_1.expect)((0, selectorList_1.parseSelectorListQuery)({})).to.deep.equal({
24
+ tab: 'fix',
25
+ days: 30,
26
+ q: '',
27
+ platform: null,
28
+ method: null,
29
+ sort: 'heals',
30
+ page: 1,
31
+ pageSize: 50,
32
+ });
33
+ (0, chai_1.expect)((0, selectorList_1.parseSelectorListQuery)({
34
+ tab: 'muted',
35
+ days: '7',
36
+ q: ' confirm ',
37
+ platform: 'ios',
38
+ method: 'LLM',
39
+ sort: 'time',
40
+ page: '3',
41
+ pageSize: '20',
42
+ })).to.deep.equal({
43
+ tab: 'muted',
44
+ days: 7,
45
+ q: 'confirm',
46
+ platform: 'ios',
47
+ method: 'LLM',
48
+ sort: 'time',
49
+ page: 3,
50
+ pageSize: 20,
51
+ });
52
+ (0, chai_1.expect)((0, selectorList_1.parseSelectorListQuery)({
53
+ tab: 'active',
54
+ days: '9999',
55
+ sort: 'random',
56
+ page: '-2',
57
+ pageSize: '1000',
58
+ })).to.include({ tab: 'fix', days: 365, sort: 'heals', page: 1, pageSize: 100 });
59
+ });
60
+ });
61
+ describe('GET /healing/selectors (real queries)', function () {
62
+ this.timeout(90000);
63
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
64
+ before(() => __awaiter(this, void 0, void 0, function* () {
65
+ yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
66
+ }));
67
+ const list = (auth, qs = '') => (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(auth)).get(`/healing/selectors${qs}`);
68
+ const names = (res) => res.body.items.map((i) => i.selector);
69
+ it('lists what to fix, most heals first, with every tab counted', () => __awaiter(this, void 0, void 0, function* () {
70
+ const res = yield list(selector_health_fixture_1.ADMIN);
71
+ (0, chai_1.expect)(res.status).to.equal(200);
72
+ (0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.hot, selector_health_fixture_1.SEL.bOnly, selector_health_fixture_1.SEL.warm, selector_health_fixture_1.SEL.legacy, selector_health_fixture_1.SEL.odd]);
73
+ (0, chai_1.expect)(res.body.counts).to.deep.equal({ fix: 5, verifying: 1, fixed: 1, muted: 2 });
74
+ (0, chai_1.expect)(res.body).to.include({
75
+ tab: 'fix',
76
+ days: 30,
77
+ page: 1,
78
+ pageSize: 50,
79
+ total: 5,
80
+ canAct: true,
81
+ });
82
+ }));
83
+ it('describes each selector: heals, sessions, time, method, suggestion and status', () => __awaiter(this, void 0, void 0, function* () {
84
+ const hot = (yield list(selector_health_fixture_1.ADMIN)).body.items[0];
85
+ (0, chai_1.expect)(hot).to.deep.include({
86
+ strategy: 'xpath',
87
+ selector: selector_health_fixture_1.SEL.hot,
88
+ heals: 4,
89
+ sessions: 2,
90
+ timeSpentMs: 18000,
91
+ topMethod: 'Visual AI',
92
+ });
93
+ (0, chai_1.expect)(hot.suggestion).to.deep.equal({ selector: selector_health_fixture_1.FIX.hot, strategy: 'xpath', share: 0.75 });
94
+ (0, chai_1.expect)(hot.state).to.include({ status: 'active', brokeAgain: 2 });
95
+ (0, chai_1.expect)(Date.now() - Date.parse(hot.lastHealedAt)).to.be.within(0.9 * selector_health_fixture_1.HOUR, 1.5 * selector_health_fixture_1.HOUR);
96
+ }));
97
+ it('lists a heal with no strategy under an empty one', () => __awaiter(this, void 0, void 0, function* () {
98
+ const legacy = (yield list(selector_health_fixture_1.ADMIN)).body.items.find((i) => i.selector === selector_health_fixture_1.SEL.legacy);
99
+ (0, chai_1.expect)(legacy).to.deep.include({ strategy: '', heals: 1 });
100
+ }));
101
+ it('shows a member only selectors healed in sessions they can see, and counts only those', () => __awaiter(this, void 0, void 0, function* () {
102
+ const res = yield list(selector_health_fixture_1.MEMBER_A);
103
+ (0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.hot, selector_health_fixture_1.SEL.warm, selector_health_fixture_1.SEL.legacy, selector_health_fixture_1.SEL.odd]);
104
+ (0, chai_1.expect)(res.body.counts).to.deep.equal({ fix: 4, verifying: 1, fixed: 1, muted: 1 });
105
+ }));
106
+ it('sorts by most recent and by most time spent healing', () => __awaiter(this, void 0, void 0, function* () {
107
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?sort=recent'))).to.deep.equal([
108
+ selector_health_fixture_1.SEL.hot,
109
+ selector_health_fixture_1.SEL.legacy,
110
+ selector_health_fixture_1.SEL.odd,
111
+ selector_health_fixture_1.SEL.bOnly,
112
+ selector_health_fixture_1.SEL.warm,
113
+ ]);
114
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?sort=time'))).to.deep.equal([
115
+ selector_health_fixture_1.SEL.warm,
116
+ selector_health_fixture_1.SEL.hot,
117
+ selector_health_fixture_1.SEL.bOnly,
118
+ selector_health_fixture_1.SEL.legacy,
119
+ selector_health_fixture_1.SEL.odd,
120
+ ]);
121
+ }));
122
+ it('searches the selector and its suggested fix, taking the text literally', () => __awaiter(this, void 0, void 0, function* () {
123
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?q=CONFIRM'))).to.deep.equal([selector_health_fixture_1.SEL.hot]);
124
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, `?q=${encodeURIComponent('cart_total_v2')}`))).to.deep.equal([
125
+ selector_health_fixture_1.SEL.warm,
126
+ ]);
127
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, `?q=${encodeURIComponent('%')}`))).to.deep.equal([selector_health_fixture_1.SEL.odd]);
128
+ }));
129
+ it('filters what to fix by platform and by healing method, keeping the counts whole', () => __awaiter(this, void 0, void 0, function* () {
130
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?platform=ios'))).to.deep.equal([selector_health_fixture_1.SEL.bOnly]);
131
+ const llm = yield list(selector_health_fixture_1.ADMIN, '?method=LLM');
132
+ (0, chai_1.expect)(names(llm)).to.deep.equal([selector_health_fixture_1.SEL.hot]);
133
+ (0, chai_1.expect)(llm.body.items[0]).to.deep.include({ heals: 1, topMethod: 'LLM' });
134
+ (0, chai_1.expect)(llm.body.counts.fix).to.equal(5);
135
+ }));
136
+ it('pages, and shows the last page for a page past the end', () => __awaiter(this, void 0, void 0, function* () {
137
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?pageSize=2&page=2'))).to.deep.equal([selector_health_fixture_1.SEL.warm, selector_health_fixture_1.SEL.legacy]);
138
+ const past = yield list(selector_health_fixture_1.ADMIN, '?pageSize=2&page=99');
139
+ (0, chai_1.expect)(past.body.page).to.equal(3);
140
+ (0, chai_1.expect)(past.body.total).to.equal(5);
141
+ (0, chai_1.expect)(names(past)).to.deep.equal([selector_health_fixture_1.SEL.odd]);
142
+ }));
143
+ it('lists selectors being verified, with their progress and who marked them fixed', () => __awaiter(this, void 0, void 0, function* () {
144
+ const res = yield list(selector_health_fixture_1.MEMBER_A, '?tab=verifying');
145
+ (0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.pending]);
146
+ (0, chai_1.expect)(res.body.items[0].heals).to.equal(1);
147
+ (0, chai_1.expect)(res.body.items[0].state).to.deep.include({
148
+ status: 'pending',
149
+ cleanBuilds: 2,
150
+ fixedBy: { id: selector_health_fixture_1.USER.priya, name: 'Priya' },
151
+ });
152
+ }));
153
+ it('lists selectors fixed in the period, with or without a heal in it', () => __awaiter(this, void 0, void 0, function* () {
154
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=fixed'))).to.deep.equal([selector_health_fixture_1.SEL.fixed]);
155
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=fixed&days=90'))).to.deep.equal([selector_health_fixture_1.SEL.fixed, selector_health_fixture_1.SEL.fixedOld]);
156
+ const week = yield list(selector_health_fixture_1.ADMIN, '?tab=fixed&days=7');
157
+ (0, chai_1.expect)(names(week)).to.deep.equal([selector_health_fixture_1.SEL.fixed]);
158
+ (0, chai_1.expect)(week.body.items[0]).to.deep.include({
159
+ heals: 0,
160
+ sessions: 0,
161
+ lastHealedAt: null,
162
+ suggestion: null,
163
+ });
164
+ }));
165
+ it('names nobody for an action by a user who is gone', () => __awaiter(this, void 0, void 0, function* () {
166
+ const fixed = (yield list(selector_health_fixture_1.ADMIN, '?tab=fixed')).body.items[0];
167
+ (0, chai_1.expect)(fixed.state.fixedBy).to.deep.equal({ id: selector_health_fixture_1.USER.gone, name: null });
168
+ }));
169
+ it('lists muted selectors with who muted them and why, only those a member can see', () => __awaiter(this, void 0, void 0, function* () {
170
+ const res = yield list(selector_health_fixture_1.MEMBER_A, '?tab=muted');
171
+ (0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.muted]);
172
+ (0, chai_1.expect)(res.body.items[0].state).to.deep.include({
173
+ status: 'muted',
174
+ mutedBy: { id: selector_health_fixture_1.USER.priya, name: 'Priya' },
175
+ muteReason: 'Screen being redesigned',
176
+ });
177
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=muted'))).to.deep.equal([selector_health_fixture_1.SEL.muted, selector_health_fixture_1.SEL.mutedB]);
178
+ }));
179
+ it('searches a status tab by selector', () => __awaiter(this, void 0, void 0, function* () {
180
+ (0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=muted&q=team-b'))).to.deep.equal([selector_health_fixture_1.SEL.mutedB]);
181
+ }));
182
+ it('tells a caller without the sessions scope that they cannot act', () => __awaiter(this, void 0, void 0, function* () {
183
+ (0, chai_1.expect)((yield list(selector_health_fixture_1.READ_ONLY_A)).body.canAct).to.equal(false);
184
+ }));
185
+ });
@@ -23,14 +23,18 @@ const SocketEvents_1 = require("../../src/enums/SocketEvents");
23
23
  * the resolved value explicitly.
24
24
  */
25
25
  function buildPrismaStub() {
26
- return {
26
+ const stub = {
27
27
  selectorState: {
28
28
  findUnique: sinon_1.default.stub(),
29
29
  upsert: sinon_1.default.stub(),
30
30
  update: sinon_1.default.stub(),
31
31
  delete: sinon_1.default.stub(),
32
32
  },
33
+ selectorEvent: { create: sinon_1.default.stub().resolves({}) },
33
34
  };
35
+ // A transaction runs its callback against the same tables, as Prisma's does.
36
+ stub.$transaction = sinon_1.default.stub().callsFake((fn) => fn(stub));
37
+ return stub;
34
38
  }
35
39
  function buildSocketStub() {
36
40
  return {
@@ -410,4 +414,73 @@ describe('SelectorStateService', () => {
410
414
  (0, chai_1.expect)(result).to.equal(null);
411
415
  }));
412
416
  });
417
+ describe('what each change records', () => {
418
+ const ctx = { strategy: STRATEGY, selector: SELECTOR, apiKeyId: API_KEY_ID, userId: 'user-1' };
419
+ const recorded = () => prismaStub.selectorEvent.create.firstCall.args[0].data;
420
+ it('records who marked it fixed, in the same transaction as the status', () => __awaiter(void 0, void 0, void 0, function* () {
421
+ prismaStub.selectorState.findUnique.resolves(null);
422
+ prismaStub.selectorState.upsert.resolves(makeRow({ status: 'pending', fixed_at: new Date() }));
423
+ yield svc.markFixed(ctx);
424
+ (0, chai_1.expect)(prismaStub.$transaction.calledOnce).to.equal(true);
425
+ (0, chai_1.expect)(recorded()).to.deep.equal({
426
+ original_strategy: STRATEGY,
427
+ original_selector: SELECTOR,
428
+ action: 'marked_fixed',
429
+ user_id: 'user-1',
430
+ reason: null,
431
+ });
432
+ (0, chai_1.expect)(prismaStub.selectorEvent.create.calledAfter(prismaStub.selectorState.upsert)).to.equal(true);
433
+ }));
434
+ it('fails the change, and tells nobody, when its record cannot be written', () => __awaiter(void 0, void 0, void 0, function* () {
435
+ prismaStub.selectorState.findUnique.resolves(null);
436
+ prismaStub.selectorState.upsert.resolves(makeRow({ status: 'pending' }));
437
+ prismaStub.selectorEvent.create.rejects(new Error('disk full'));
438
+ let thrown;
439
+ try {
440
+ yield svc.markFixed(ctx);
441
+ }
442
+ catch (e) {
443
+ thrown = e;
444
+ }
445
+ (0, chai_1.expect)(thrown.message).to.equal('disk full');
446
+ (0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
447
+ }));
448
+ it('records the mute with its reason', () => __awaiter(void 0, void 0, void 0, function* () {
449
+ prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted', muted_at: new Date() }));
450
+ yield svc.mute(Object.assign(Object.assign({}, ctx), { reason: 'Screen being redesigned' }));
451
+ (0, chai_1.expect)(recorded()).to.include({
452
+ action: 'muted',
453
+ user_id: 'user-1',
454
+ reason: 'Screen being redesigned',
455
+ });
456
+ }));
457
+ it('records an unmute, and nothing for a selector that is not muted', () => __awaiter(void 0, void 0, void 0, function* () {
458
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted', muted_at: new Date() }));
459
+ prismaStub.selectorState.delete.resolves(makeRow());
460
+ yield svc.unmute(ctx);
461
+ (0, chai_1.expect)(recorded()).to.include({ action: 'unmuted', user_id: 'user-1' });
462
+ prismaStub.selectorEvent.create.resetHistory();
463
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'active' }));
464
+ yield svc.unmute(ctx);
465
+ (0, chai_1.expect)(prismaStub.selectorEvent.create.called).to.equal(false);
466
+ }));
467
+ it('records a cancelled verification', () => __awaiter(void 0, void 0, void 0, function* () {
468
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending', fixed_at: new Date() }));
469
+ prismaStub.selectorState.delete.resolves(makeRow());
470
+ yield svc.cancelVerification(ctx);
471
+ (0, chai_1.expect)(recorded()).to.include({ action: 'verification_cancelled', user_id: 'user-1' });
472
+ }));
473
+ it('records a selector breaking again, with nobody as its person', () => __awaiter(void 0, void 0, void 0, function* () {
474
+ prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'resolved', resolved_at: new Date() }));
475
+ prismaStub.selectorState.update.resolves(makeRow({ status: 'active', regression_count: 1 }));
476
+ yield svc.onHealRecorded({ strategy: STRATEGY, selector: SELECTOR, sessionId: SESSION_ID });
477
+ (0, chai_1.expect)(recorded()).to.include({ action: 'broke_again', user_id: null, reason: null });
478
+ }));
479
+ it('records nothing for a heal on a selector with no status', () => __awaiter(void 0, void 0, void 0, function* () {
480
+ prismaStub.selectorState.findUnique.resolves(null);
481
+ yield svc.onHealRecorded({ strategy: STRATEGY, selector: SELECTOR, sessionId: SESSION_ID });
482
+ (0, chai_1.expect)(prismaStub.$transaction.called).to.equal(false);
483
+ (0, chai_1.expect)(prismaStub.selectorEvent.create.called).to.equal(false);
484
+ }));
485
+ });
413
486
  });
@@ -40,8 +40,12 @@ describe('SelectorVerificationJob.run', () => {
40
40
  findMany: sinon_1.default.stub(),
41
41
  update: sinon_1.default.stub().resolves({}),
42
42
  },
43
+ selectorEvent: { create: sinon_1.default.stub().resolves({}) },
43
44
  $queryRaw: sinon_1.default.stub(),
44
45
  };
46
+ prismaStub.$transaction = sinon_1.default
47
+ .stub()
48
+ .callsFake((fn) => fn(prismaStub));
45
49
  socketStub = { emitToDashboard: sinon_1.default.stub() };
46
50
  job = new SelectorVerificationJob_1.SelectorVerificationJob(prismaStub, socketStub);
47
51
  });
@@ -203,4 +207,38 @@ describe('SelectorVerificationJob.run', () => {
203
207
  yield job.run();
204
208
  (0, chai_1.expect)(prismaStub.$queryRaw.called).to.be.false;
205
209
  }));
210
+ it('records the verification, with nobody as its person', () => __awaiter(void 0, void 0, void 0, function* () {
211
+ prismaStub.selectorState.findMany.resolves([
212
+ {
213
+ id: 'r-1',
214
+ original_strategy: 'xpath',
215
+ original_selector: '//x',
216
+ status: 'pending',
217
+ fixed_at: new Date('2026-04-20T00:00:00Z'),
218
+ clean_builds_count: 2,
219
+ },
220
+ ]);
221
+ prismaStub.$queryRaw.resolves([
222
+ { build_id: 'b-1', healed: 0 },
223
+ { build_id: 'b-2', healed: 0 },
224
+ { build_id: 'b-3', healed: 0 },
225
+ ]);
226
+ prismaStub.selectorState.update.resolves({
227
+ id: 'r-1',
228
+ original_strategy: 'xpath',
229
+ original_selector: '//x',
230
+ status: 'resolved',
231
+ clean_builds_count: 3,
232
+ resolved_at: new Date(),
233
+ });
234
+ yield job.run();
235
+ (0, chai_1.expect)(prismaStub.selectorEvent.create.calledOnce).to.equal(true);
236
+ (0, chai_1.expect)(prismaStub.selectorEvent.create.firstCall.args[0].data).to.deep.equal({
237
+ original_strategy: 'xpath',
238
+ original_selector: '//x',
239
+ action: 'verified',
240
+ user_id: null,
241
+ reason: null,
242
+ });
243
+ }));
206
244
  });
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const androidMetrics_1 = require("../../src/services/metrics/androidMetrics");
5
+ const iosMetrics_1 = require("../../src/services/metrics/iosMetrics");
6
+ /**
7
+ * Recorded on the S9+ (Android 10) with the command androidSampleCommand
8
+ * builds; the second reading is the first plus 1000 jiffies, 800 of them
9
+ * idle, and 50 for the app.
10
+ */
11
+ const FIRST = [
12
+ 'cpu 4842746 1486246 9435122 410569805 191176 0 106298 0 0 0',
13
+ 'MemTotal: 5755748 kB',
14
+ 'MemAvailable: 2839168 kB',
15
+ 'pid=1404',
16
+ 'stat=211391 93546',
17
+ 'VmRSS:\t 334480 kB',
18
+ ].join('\n');
19
+ const SECOND = [
20
+ 'cpu 4842846 1486246 9435222 410570595 191186 0 106298 0 0 0',
21
+ 'MemTotal: 5755748 kB',
22
+ 'MemAvailable: 2838144 kB',
23
+ 'pid=1404',
24
+ 'stat=211421 93566',
25
+ 'VmRSS:\t 335504 kB',
26
+ ].join('\n');
27
+ /** A reading the test knows is readable. */
28
+ function read(out) {
29
+ const r = (0, androidMetrics_1.parseAndroidReading)(out);
30
+ if (!r)
31
+ throw new Error('unreadable sample');
32
+ return r;
33
+ }
34
+ describe('session metrics: Android readings', () => {
35
+ it('reads device CPU jiffies, memory, and the app process', () => {
36
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)(FIRST)).to.deep.equal({
37
+ cpuTotal: 4842746 + 1486246 + 9435122 + 410569805 + 191176 + 0 + 106298 + 0,
38
+ cpuIdle: 410569805 + 191176,
39
+ memTotalKb: 5755748,
40
+ memAvailableKb: 2839168,
41
+ pid: 1404,
42
+ appJiffies: 211391 + 93546,
43
+ appRssKb: 334480,
44
+ });
45
+ });
46
+ it('reads a device with no app process as device figures only', () => {
47
+ const out = [FIRST.split('\n')[0], FIRST.split('\n')[1], FIRST.split('\n')[2], 'pid='].join('\n');
48
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)(out)).to.include({ pid: null, appJiffies: null, appRssKb: null });
49
+ });
50
+ it('reads output with CRLF line ends', () => {
51
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)(FIRST.replace(/\n/g, '\r\n'))).to.include({
52
+ pid: 1404,
53
+ appRssKb: 334480,
54
+ });
55
+ });
56
+ it('gives up on output without the cpu line', () => {
57
+ (0, chai_1.expect)((0, androidMetrics_1.parseAndroidReading)('MemTotal: 5755748 kB')).to.equal(null);
58
+ });
59
+ it('records no CPU on the first sample, only memory', () => {
60
+ const s = (0, androidMetrics_1.androidSample)(null, read(FIRST), 'com.android.systemui', 1000);
61
+ (0, chai_1.expect)(s).to.deep.equal({
62
+ at: 1000,
63
+ deviceCpuPct: null,
64
+ deviceMemMb: 2848.2,
65
+ deviceMemTotalMb: 5620.8,
66
+ appCpuPct: null,
67
+ appMemMb: 326.6,
68
+ appId: 'com.android.systemui',
69
+ });
70
+ });
71
+ it('turns two readings into device and app CPU as shares of the whole device', () => {
72
+ const s = (0, androidMetrics_1.androidSample)((0, androidMetrics_1.parseAndroidReading)(FIRST), read(SECOND), 'com.android.systemui', 3000);
73
+ (0, chai_1.expect)(s.deviceCpuPct).to.equal(20);
74
+ (0, chai_1.expect)(s.appCpuPct).to.equal(5);
75
+ (0, chai_1.expect)(s.appMemMb).to.equal(327.6);
76
+ });
77
+ it('records no app CPU, not a spike, when the app restarted between samples', () => {
78
+ const restarted = SECOND.replace('pid=1404', 'pid=2210').replace('stat=211421 93566', 'stat=12 8');
79
+ const s = (0, androidMetrics_1.androidSample)((0, androidMetrics_1.parseAndroidReading)(FIRST), read(restarted), 'com.android.systemui', 3000);
80
+ (0, chai_1.expect)(s.appCpuPct).to.equal(null);
81
+ (0, chai_1.expect)(s.deviceCpuPct).to.equal(20);
82
+ (0, chai_1.expect)(s.appMemMb).to.equal(327.6);
83
+ });
84
+ it('names no app when the app has no process', () => {
85
+ const none = [SECOND.split('\n')[0], SECOND.split('\n')[1], SECOND.split('\n')[2], 'pid='].join('\n');
86
+ const s = (0, androidMetrics_1.androidSample)((0, androidMetrics_1.parseAndroidReading)(FIRST), read(none), 'com.acme.shop', 3000);
87
+ (0, chai_1.expect)(s).to.include({ appId: null, appCpuPct: null, appMemMb: null, deviceCpuPct: 20 });
88
+ });
89
+ });
90
+ describe('session metrics: Android package names', () => {
91
+ it('accepts package names and nothing else', () => {
92
+ for (const ok of ['com.acme.shop', 'com.sec.android.app.launcher', 'io.x_y.App2']) {
93
+ (0, chai_1.expect)((0, androidMetrics_1.isSafePackage)(ok), ok).to.equal(true);
94
+ }
95
+ for (const bad of [
96
+ '',
97
+ 'shop',
98
+ 'com.acme;reboot',
99
+ '$(id).x',
100
+ 'com.acme shop',
101
+ 'com..x',
102
+ '1com.x',
103
+ undefined,
104
+ 42,
105
+ ]) {
106
+ (0, chai_1.expect)((0, androidMetrics_1.isSafePackage)(bad), String(bad)).to.equal(false);
107
+ }
108
+ });
109
+ it("reads a session's appPackage from either capability spelling", () => {
110
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)({ 'appium:appPackage': 'com.a.b' })).to.equal('com.a.b');
111
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)({ appPackage: 'com.c.d' })).to.equal('com.c.d');
112
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)({})).to.equal(undefined);
113
+ (0, chai_1.expect)((0, androidMetrics_1.appPackageOf)(undefined)).to.equal(undefined);
114
+ });
115
+ it('builds a device-only command with no package, and refuses anything but a package', () => {
116
+ (0, chai_1.expect)((0, androidMetrics_1.androidSampleCommand)(null)).to.not.include('pidof');
117
+ (0, chai_1.expect)((0, androidMetrics_1.androidSampleCommand)('com.acme.shop')).to.include('pidof com.acme.shop');
118
+ (0, chai_1.expect)(() => (0, androidMetrics_1.androidSampleCommand)('com.acme;reboot')).to.throw(/not a package name/);
119
+ });
120
+ it('reads the foreground app from dumpsys, before and after Android 10', () => {
121
+ (0, chai_1.expect)((0, androidMetrics_1.parseForegroundPackage)(' mResumedActivity: ActivityRecord{c51fd83 u0 com.sec.android.app.launcher/.activities.LauncherActivity t2}')).to.equal('com.sec.android.app.launcher');
122
+ (0, chai_1.expect)((0, androidMetrics_1.parseForegroundPackage)(' topResumedActivity=ActivityRecord{a1b2c3 u0 com.acme.shop/.MainActivity t12}')).to.equal('com.acme.shop');
123
+ (0, chai_1.expect)((0, androidMetrics_1.parseForegroundPackage)('nothing resumed')).to.equal(null);
124
+ });
125
+ });
126
+ describe('session metrics: iPhone sysmontap lines', () => {
127
+ it('reads device CPU, the load divided over the cores', () => {
128
+ const line = '{"time":"2026-10-02T11:56:46.322036+05:30","level":"INFO","msg":"received CPU usage data","cpu_count":6,"enabled_cpus":6,"end_time":1301586447413,"cpu_total_load":77.48226950354608}';
129
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)(line)).to.equal(12.9);
130
+ });
131
+ it('reads nothing from other lines', () => {
132
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)('{"level":"WARN","msg":"go-ios agent is not running."}')).to.equal(null);
133
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)('not json')).to.equal(null);
134
+ });
135
+ it('never reports more than 100%', () => {
136
+ (0, chai_1.expect)((0, iosMetrics_1.parseSysmontapCpu)('{"enabled_cpus":2,"cpu_total_load":450}')).to.equal(100);
137
+ });
138
+ });
@@ -0,0 +1,140 @@
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 dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
20
+ const metricsBody_1 = require("../../src/services/metrics/metricsBody");
21
+ const scratch_database_1 = require("../helpers/scratch-database");
22
+ const row = (at, over = {}) => (Object.assign({ at, device_cpu_pct: 20, device_mem_mb: 2800, device_mem_total: 5620.8, app_cpu_pct: 5, app_mem_mb: 300, app_id: 'com.acme.shop' }, over));
23
+ describe('session metrics: the response', () => {
24
+ it('says what each platform can record', () => {
25
+ (0, chai_1.expect)((0, metricsBody_1.seriesFor)('Android')).to.deep.equal({
26
+ deviceCpu: true,
27
+ deviceMem: true,
28
+ appCpu: true,
29
+ appMem: true,
30
+ });
31
+ (0, chai_1.expect)((0, metricsBody_1.seriesFor)('ios')).to.deep.equal({
32
+ deviceCpu: true,
33
+ deviceMem: false,
34
+ appCpu: false,
35
+ appMem: false,
36
+ });
37
+ (0, chai_1.expect)((0, metricsBody_1.seriesFor)('')).to.deep.equal({
38
+ deviceCpu: false,
39
+ deviceMem: false,
40
+ appCpu: false,
41
+ appMem: false,
42
+ });
43
+ });
44
+ it('passes on whether a running session is being sampled, and nothing for an ended one', () => {
45
+ (0, chai_1.expect)((0, metricsBody_1.sessionMetricsBody)('android', [], 'off').recording).to.equal('off');
46
+ (0, chai_1.expect)((0, metricsBody_1.sessionMetricsBody)('android', []).recording).to.equal(null);
47
+ });
48
+ it('answers samples in time order, named by the last app recorded', () => {
49
+ const body = (0, metricsBody_1.sessionMetricsBody)('android', [
50
+ row(4000, { app_id: null, app_cpu_pct: null, app_mem_mb: null }),
51
+ row(0, { app_id: 'com.old.app' }),
52
+ row(2000),
53
+ ]);
54
+ (0, chai_1.expect)(body.samples.map((s) => s.t)).to.deep.equal([0, 2000, 4000]);
55
+ (0, chai_1.expect)(body.appId).to.equal('com.acme.shop');
56
+ (0, chai_1.expect)(body.intervalMs).to.equal(2000);
57
+ (0, chai_1.expect)(body.samples[1]).to.deep.equal({
58
+ t: 2000,
59
+ deviceCpu: 20,
60
+ deviceMemMb: 2800,
61
+ deviceMemTotalMb: 5620.8,
62
+ appCpu: 5,
63
+ appMemMb: 300,
64
+ app: 'com.acme.shop',
65
+ });
66
+ });
67
+ it("names each sample's app, since the foreground app can change", () => {
68
+ const body = (0, metricsBody_1.sessionMetricsBody)('android', [
69
+ row(0, { app_id: 'com.a.one' }),
70
+ row(2000, { app_id: 'com.b.two' }),
71
+ ]);
72
+ (0, chai_1.expect)(body.samples.map((s) => s.app)).to.deep.equal(['com.a.one', 'com.b.two']);
73
+ });
74
+ });
75
+ describe('GET /session/:sessionId/metrics', function () {
76
+ this.timeout(60000);
77
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
78
+ function app() {
79
+ const a = (0, express_1.default)();
80
+ a.use((req, _res, next) => {
81
+ req.auth = {
82
+ kind: 'user',
83
+ userId: 'sa',
84
+ role: 'SUPER_ADMIN',
85
+ scopes: ['admin', 'devices', 'sessions', 'read'],
86
+ teamIds: undefined,
87
+ };
88
+ next();
89
+ });
90
+ dashboard_1.default.register(a);
91
+ return a;
92
+ }
93
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
94
+ yield scratch.db.sessionMetric.deleteMany({});
95
+ yield scratch.db.session.deleteMany({});
96
+ }));
97
+ it("answers a session's samples", () => __awaiter(this, void 0, void 0, function* () {
98
+ yield scratch.db.session.create({
99
+ data: {
100
+ id: 'metrics-1',
101
+ desired_capabilities: '{}',
102
+ session_capabilities: '{}',
103
+ node_id: 'n',
104
+ has_live_video: false,
105
+ device_udid: 'phone-m',
106
+ device_platform: 'android',
107
+ device_version: '10',
108
+ },
109
+ });
110
+ yield scratch.db.sessionMetric.createMany({
111
+ data: [
112
+ Object.assign({ session_id: 'metrics-1' }, row(2000)),
113
+ Object.assign({ session_id: 'metrics-1' }, row(0)),
114
+ ],
115
+ });
116
+ const res = yield (0, supertest_1.default)(app()).get('/session/metrics-1/metrics');
117
+ (0, chai_1.expect)(res.status).to.equal(200);
118
+ (0, chai_1.expect)(res.body.platform).to.equal('android');
119
+ (0, chai_1.expect)(res.body.samples.map((s) => s.t)).to.deep.equal([0, 2000]);
120
+ (0, chai_1.expect)(res.body.series.appMem).to.equal(true);
121
+ }));
122
+ it('says a running session nothing samples is not recorded', () => __awaiter(this, void 0, void 0, function* () {
123
+ yield scratch.db.session.create({
124
+ data: {
125
+ id: 'metrics-2',
126
+ status: 'running',
127
+ desired_capabilities: '{}',
128
+ session_capabilities: '{}',
129
+ node_id: 'n',
130
+ has_live_video: false,
131
+ device_udid: 'sim-1',
132
+ device_platform: 'ios',
133
+ device_version: '18.0',
134
+ },
135
+ });
136
+ const res = yield (0, supertest_1.default)(app()).get('/session/metrics-2/metrics');
137
+ (0, chai_1.expect)(res.status).to.equal(200);
138
+ (0, chai_1.expect)(res.body.recording).to.equal('off');
139
+ }));
140
+ });