@xenon-device-management/xenon 2.9.2 → 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 (143) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-B87n4pUu.js → AnnotationOverlay-LzfXiiZJ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-BzFDp4mK.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-BpgWvmZo.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-CCB-egSH.js} +2 -2
  6. package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-DkUVmANo.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-mbfnfTql.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-BH5usz4Y.js} +1 -1
  9. package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Cbo9mdTk.js} +1 -1
  10. package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-D6XfQHPr.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-CmFPkWqH.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CSlKk2qC.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-BFW9Dlqd.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-C4DioTTi.js} +1 -1
  15. package/lib/public/assets/{Table-D_2lhvRM.js → Table-DrKjlL00.js} +1 -1
  16. package/lib/public/assets/{activity-cNHHOKx5.js → activity-C9bE7Jm8.js} +1 -1
  17. package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DRYic4oO.js} +1 -1
  18. package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-CaQU0g7c.js} +1 -1
  19. package/lib/public/assets/{apps-DCiI3UOb.js → apps-c1gUFmmK.js} +1 -1
  20. package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-DMvAWA8r.js} +1 -1
  21. package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-BEQgxmKT.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-Ba0e2GeU.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-COODQnc7.js} +2 -2
  24. package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-DZQgWBUs.js} +4 -4
  25. package/lib/public/assets/{button-C0caCjJB.js → button-WW9j43wR.js} +1 -1
  26. package/lib/public/assets/{calendar-BqOOkln_.js → calendar-COUs9VxF.js} +1 -1
  27. package/lib/public/assets/{check-DYSXRG79.js → check-D8ifHIJR.js} +1 -1
  28. package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-BjKVUlB_.js} +1 -1
  29. package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-CfP79QE2.js} +1 -1
  30. package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-BZbFCixZ.js} +1 -1
  31. package/lib/public/assets/{clock-XP3iBAhg.js → clock-BuTm35Qg.js} +1 -1
  32. package/lib/public/assets/{copy-D8d9UzWE.js → copy-W00zUBwN.js} +1 -1
  33. package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-BEpS-Y-_.js} +5 -5
  34. package/lib/public/assets/{download-SaN9-umX.js → download-CGz-MS3y.js} +1 -1
  35. package/lib/public/assets/{forgot-password-DfUhEZW5.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-IefGCFQQ.js → index-DWHTp1YP.js} +1 -1
  38. package/lib/public/assets/{index-CzaIyL9t.js → index-EkaQM1DK.js} +2 -2
  39. package/lib/public/assets/{input-BqFzAQT3.js → input-ChaTXeCq.js} +1 -1
  40. package/lib/public/assets/{keycodes-DDV2OlpQ.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-Eye1CG8p.js → lock-B2JaqqRI.js} +1 -1
  44. package/lib/public/assets/{login-Co1IXuen.js → login-BEWIfLfC.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-BGJIJQ-k.js} +1 -1
  46. package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-BrwMY4G0.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
  48. package/lib/public/assets/{network-BP_U4iDZ.js → network-DfIhNVJv.js} +1 -1
  49. package/lib/public/assets/overview-B1mePY2_.js +6 -0
  50. package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DoD3TOzZ.js} +1 -1
  51. package/lib/public/assets/{plus-1k0sxXyU.js → plus-BGvWvpBB.js} +1 -1
  52. package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-CvV5aWvl.js} +1 -1
  53. package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-CGVd31LR.js} +1 -1
  54. package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page-Bq2cvwb5.js} +1 -1
  55. package/lib/public/assets/{select-CiRW6v17.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-DhwZoWv7.js → settings-C-EnGIqs.js} +1 -1
  60. package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
  61. package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-B4cFM-5B.js} +1 -1
  62. package/lib/public/assets/{teams-D0IBa2Ov.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-u8MlXUBD.js → trash-2-CJCKBOP9.js} +1 -1
  65. package/lib/public/assets/{upload-Cq8L3kpw.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-Dbr2HEfw.js → use-password-reset-mode-iPen3tmC.js} +1 -1
  68. package/lib/public/assets/{users-CZqCE511.js → users-CrcjmRdf.js} +1 -1
  69. package/lib/public/assets/{users-DxLHYmFm.js → users-DBUv0CJx.js} +1 -1
  70. package/lib/public/assets/{video-off-mh1EEzP9.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-BYuOib-Q.js → webhook-settings-CQoSshBn.js} +1 -1
  73. package/lib/public/assets/{zap-BOpUur0M.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/generated/client/edge.js +27 -3
  81. package/lib/src/generated/client/index-browser.js +24 -0
  82. package/lib/src/generated/client/index.d.ts +8207 -5283
  83. package/lib/src/generated/client/index.js +27 -3
  84. package/lib/src/generated/client/package.json +1 -1
  85. package/lib/src/generated/client/schema.prisma +43 -9
  86. package/lib/src/generated/client/wasm.js +24 -0
  87. package/lib/src/prisma.js +2 -0
  88. package/lib/src/services/CleanupService.js +1 -0
  89. package/lib/src/services/NotificationService.js +3 -4
  90. package/lib/src/services/SelectorStateService.js +151 -113
  91. package/lib/src/services/SelectorVerificationJob.js +14 -9
  92. package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
  93. package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
  94. package/lib/src/services/metrics/SessionMetricsService.js +238 -0
  95. package/lib/src/services/metrics/androidMetrics.js +94 -0
  96. package/lib/src/services/metrics/iosMetrics.js +26 -0
  97. package/lib/src/services/metrics/metricsBody.js +37 -0
  98. package/lib/src/services/metrics/types.js +19 -0
  99. package/lib/src/services/selector-health/access.js +66 -0
  100. package/lib/src/services/selector-health/healingTrend.js +49 -0
  101. package/lib/src/services/selector-health/people.js +27 -0
  102. package/lib/src/services/selector-health/selectorDetail.js +198 -0
  103. package/lib/src/services/selector-health/selectorKeys.js +36 -0
  104. package/lib/src/services/selector-health/selectorList.js +333 -0
  105. package/lib/src/services/selector-health/stateView.js +67 -0
  106. package/lib/test/helpers/scratch-database.js +2 -0
  107. package/lib/test/helpers/selector-health-fixture.js +216 -0
  108. package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
  109. package/lib/test/unit/CleanupService.spec.js +28 -2
  110. package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
  111. package/lib/test/unit/healing-no-cost.spec.js +134 -0
  112. package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
  113. package/lib/test/unit/healing-trend.spec.js +118 -0
  114. package/lib/test/unit/selector-event-model.spec.js +47 -0
  115. package/lib/test/unit/selector-health-actions.spec.js +106 -0
  116. package/lib/test/unit/selector-health-detail.spec.js +131 -0
  117. package/lib/test/unit/selector-health-list.spec.js +185 -0
  118. package/lib/test/unit/selector-state-service.spec.js +74 -1
  119. package/lib/test/unit/selector-verification-job.spec.js +38 -0
  120. package/lib/test/unit/session-metrics-parse.spec.js +138 -0
  121. package/lib/test/unit/session-metrics-route.spec.js +140 -0
  122. package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
  123. package/lib/test/unit/session-metrics-service.spec.js +215 -0
  124. package/lib/tsconfig.tsbuildinfo +1 -1
  125. package/package.json +1 -1
  126. package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
  127. package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
  128. package/prisma/schema.prisma +43 -9
  129. package/schema.json +5 -0
  130. package/lib/public/assets/chevron-left-BSG6mcpo.js +0 -6
  131. package/lib/public/assets/funnel-OefHE-54.js +0 -6
  132. package/lib/public/assets/layers-a3GNz2DA.js +0 -6
  133. package/lib/public/assets/list-checks-DMrMAFDt.js +0 -6
  134. package/lib/public/assets/overview-CsPyPv10.js +0 -1
  135. package/lib/public/assets/selector-detail-page-pA913STP.js +0 -1
  136. package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
  137. package/lib/public/assets/selector-health-CHLX8KqY.js +0 -16
  138. package/lib/public/assets/selector-health-page-vVkg8Mii.js +0 -16
  139. package/lib/public/assets/session-detail-page-BCD49zDi.js +0 -4
  140. package/lib/public/assets/stat-tile-BaE76cnK.js +0 -8
  141. package/lib/public/assets/time-B8FIgwPE.js +0 -1
  142. package/lib/src/profiling/AndroidAppProfiler.js +0 -213
  143. /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
@@ -1,14 +1,29 @@
1
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
+ };
2
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
3
16
  const chai_1 = require("chai");
4
- const CleanupService_1 = require("../../src/services/CleanupService");
17
+ const sinon_1 = __importDefault(require("sinon"));
5
18
  const typedi_1 = require("typedi");
6
- require("reflect-metadata");
19
+ const CleanupService_1 = require("../../src/services/CleanupService");
20
+ const prisma_1 = require("../../src/prisma");
7
21
  describe('CleanupService Unit Tests', () => {
8
22
  let cleanupService;
9
23
  beforeEach(() => {
10
24
  cleanupService = typedi_1.Container.get(CleanupService_1.CleanupService);
11
25
  });
26
+ afterEach(() => sinon_1.default.restore());
12
27
  it('should be instantiable via TypeDI', () => {
13
28
  (0, chai_1.expect)(cleanupService).to.be.an.instanceOf(CleanupService_1.CleanupService);
14
29
  });
@@ -18,4 +33,15 @@ describe('CleanupService Unit Tests', () => {
18
33
  it('should have purgeBuild method', () => {
19
34
  (0, chai_1.expect)(cleanupService.purgeBuild).to.be.a('function');
20
35
  });
36
+ it("deletes a session's CPU and memory samples with its other rows", () => __awaiter(void 0, void 0, void 0, function* () {
37
+ const deleted = [];
38
+ for (const model of ['sessionLog', 'log', 'profiling', 'sessionMetric']) {
39
+ sinon_1.default.stub(prisma_1.prisma[model], 'deleteMany').callsFake((args) => __awaiter(void 0, void 0, void 0, function* () {
40
+ deleted.push(`${model}:${args.where.session_id}`);
41
+ return { count: 0 };
42
+ }));
43
+ }
44
+ yield cleanupService.deleteSessionChildren('s-cleanup-1');
45
+ (0, chai_1.expect)(deleted).to.include('sessionMetric:s-cleanup-1');
46
+ }));
21
47
  });
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const event_manager_1 = require("../../src/dashboard/event-manager");
53
+ const assets = __importStar(require("../../src/dashboard/asset-manager"));
54
+ const sessionService = __importStar(require("../../src/dashboard/services/session-service"));
55
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
56
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
57
+ const SocketServer_1 = require("../../src/services/SocketServer");
58
+ const TracingService_1 = require("../../src/services/TracingService");
59
+ const MetricsService_1 = require("../../src/services/MetricsService");
60
+ const device_store_1 = require("../../src/data-service/device-store");
61
+ const prisma_1 = require("../../src/prisma");
62
+ const container_registration_1 = require("../helpers/container-registration");
63
+ /**
64
+ * EventManager starts a session's sampling once the session's row exists (the
65
+ * samples point at it) and stops it however the session ends, in memory or
66
+ * not.
67
+ */
68
+ describe('EventManager: CPU and memory sampling', () => {
69
+ let restore;
70
+ let metrics;
71
+ let order;
72
+ beforeEach(() => {
73
+ restore = (0, container_registration_1.saveRegistrations)(SessionMetricsService_1.SessionMetricsService, SocketServer_1.SocketServer);
74
+ order = [];
75
+ metrics = {
76
+ appliesTo: sinon_1.default.stub().returns(true),
77
+ start: sinon_1.default.stub().callsFake(() => order.push('metrics')),
78
+ stop: sinon_1.default.stub().resolves(),
79
+ };
80
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, metrics);
81
+ typedi_1.Container.set(SocketServer_1.SocketServer, {
82
+ emitToDashboard: sinon_1.default.stub(),
83
+ emitToDashboardForDevices: sinon_1.default.stub().resolves(),
84
+ hasScopedDashboard: sinon_1.default.stub().returns(false),
85
+ });
86
+ });
87
+ afterEach(() => {
88
+ sinon_1.default.restore();
89
+ restore();
90
+ });
91
+ function startSession() {
92
+ sinon_1.default.stub(assets, 'prepareDirectory');
93
+ sinon_1.default.stub(sessionService, 'getOrCreateNewBuild').resolves({ id: 'b-metrics' });
94
+ sinon_1.default.stub(TracingService_1.TracingService.prototype, 'getTraceId').returns('t-1');
95
+ sinon_1.default.stub(MetricsService_1.MetricsService.prototype, 'incrementSessionStart');
96
+ const create = sinon_1.default.stub(prisma_1.prisma.session, 'create').callsFake(() => __awaiter(this, void 0, void 0, function* () {
97
+ order.push('row');
98
+ return {};
99
+ }));
100
+ const session = {
101
+ getId: () => 's-metrics-1',
102
+ getCapabilities: () => ({ platformName: 'Android' }),
103
+ getLiveVideoUrl: () => null,
104
+ startPerformanceRecording: sinon_1.default.stub().resolves(),
105
+ apiKeyId: null,
106
+ userId: null,
107
+ };
108
+ const device = {
109
+ udid: 'phone-m',
110
+ platform: 'android',
111
+ realDevice: true,
112
+ host: 'h',
113
+ name: 'S9',
114
+ sdk: '10',
115
+ };
116
+ return { create, session, device };
117
+ }
118
+ it("starts sampling once the session's row is written, and says so on the row", () => __awaiter(void 0, void 0, void 0, function* () {
119
+ const { create, session, device } = startSession();
120
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted({}, session, device);
121
+ (0, chai_1.expect)(order).to.deep.equal(['row', 'metrics']);
122
+ (0, chai_1.expect)(metrics.start.firstCall.args[0]).to.deep.equal({
123
+ sessionId: 's-metrics-1',
124
+ device,
125
+ capabilities: { platformName: 'Android' },
126
+ });
127
+ (0, chai_1.expect)(create.firstCall.args[0].data.is_profiling_available).to.equal(true);
128
+ }));
129
+ it("doesn't sample a phone the service doesn't apply to", () => __awaiter(void 0, void 0, void 0, function* () {
130
+ metrics.appliesTo.returns(false);
131
+ const { session, device } = startSession();
132
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted({}, session, device);
133
+ (0, chai_1.expect)(metrics.start.called).to.equal(false);
134
+ }));
135
+ it('stops sampling when the session stops, even one no longer in memory', () => __awaiter(void 0, void 0, void 0, function* () {
136
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
137
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
138
+ sinon_1.default
139
+ .stub(prisma_1.prisma.session, 'findFirst')
140
+ .resolves({ id: 's-metrics-2', status: 'running', device_udid: 'phone-m' });
141
+ sinon_1.default.stub(prisma_1.prisma.session, 'update').resolves({});
142
+ sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findFirst').resolves(null);
143
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped('s-metrics-2');
144
+ (0, chai_1.expect)(metrics.stop.calledOnceWithExactly('s-metrics-2')).to.equal(true);
145
+ }));
146
+ });
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const axios_1 = __importDefault(require("axios"));
52
+ const express_1 = __importDefault(require("express"));
53
+ const supertest_1 = __importDefault(require("supertest"));
54
+ const typedi_1 = require("typedi");
55
+ const dashboard_1 = __importStar(require("../../src/app/routers/dashboard"));
56
+ const NotificationService_1 = require("../../src/services/NotificationService");
57
+ const prisma_1 = require("../../src/prisma");
58
+ const heal = (tier) => ({
59
+ session_id: 's-1',
60
+ original_strategy: 'xpath',
61
+ original_selector: '//a',
62
+ healed_strategy: 'xpath',
63
+ healed_selector: '//b',
64
+ healing_confidence: 0.9,
65
+ healing_tier: tier,
66
+ createdAt: new Date(),
67
+ duration: 1000,
68
+ });
69
+ describe('Selector Health reports no cost', () => {
70
+ afterEach(() => sinon_1.default.restore());
71
+ function app() {
72
+ const a = (0, express_1.default)();
73
+ a.use(express_1.default.json());
74
+ a.use((req, _res, next) => {
75
+ req.auth = {
76
+ kind: 'user',
77
+ userId: 'sa',
78
+ role: 'SUPER_ADMIN',
79
+ scopes: 'admin',
80
+ teamIds: undefined,
81
+ };
82
+ next();
83
+ });
84
+ dashboard_1.default.register(a);
85
+ return a;
86
+ }
87
+ it('leaves cost out of the hotspot aggregation', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findMany').resolves([heal('LLM'), heal('OCR')]);
89
+ sinon_1.default.stub(prisma_1.prisma.selectorState, 'findMany').resolves([]);
90
+ const agg = yield (0, dashboard_1.aggregateHotspots)({ windowDays: 7, limit: 10 });
91
+ (0, chai_1.expect)(agg).to.not.have.property('estCostUsd');
92
+ (0, chai_1.expect)(agg.totalHeals).to.equal(2);
93
+ }));
94
+ it('leaves cost out of the summary', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findMany').resolves([heal('LLM')]);
96
+ sinon_1.default.stub(prisma_1.prisma.selectorState, 'count').resolves(0);
97
+ const res = yield (0, supertest_1.default)(app()).get('/healing/summary?windowDays=7');
98
+ (0, chai_1.expect)(res.status).to.equal(200);
99
+ (0, chai_1.expect)(res.body.current).to.not.have.property('estCostUsd');
100
+ (0, chai_1.expect)(res.body.prior).to.not.have.property('estCostUsd');
101
+ }));
102
+ it('leaves cost out of the CI gate', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ sinon_1.default
104
+ .stub(prisma_1.prisma.sessionLog, 'findMany')
105
+ .resolves(Array.from({ length: 5 }, () => heal('LLM')));
106
+ sinon_1.default.stub(prisma_1.prisma.selectorState, 'findMany').resolves([]);
107
+ const res = yield (0, supertest_1.default)(app()).get('/healing/hotspots/violations');
108
+ (0, chai_1.expect)(res.status).to.equal(200);
109
+ (0, chai_1.expect)(res.body.violationCount).to.equal(1);
110
+ (0, chai_1.expect)(res.body).to.not.have.property('estCostUsd');
111
+ }));
112
+ it('sends a digest with no cost in it', () => __awaiter(void 0, void 0, void 0, function* () {
113
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
114
+ sinon_1.default.stub(NotificationService_1.NotificationService.prototype, 'getConfigs').resolves([
115
+ {
116
+ id: 'w-1',
117
+ url: 'https://hooks.example.test/x',
118
+ type: 'slack',
119
+ active: true,
120
+ events: JSON.stringify(['selector_health_digest']),
121
+ payloadTemplate: null,
122
+ },
123
+ ]);
124
+ yield typedi_1.Container.get(NotificationService_1.NotificationService).dispatchEvent('selector_health_digest', {
125
+ windowDays: 7,
126
+ totalHeals: 3,
127
+ distinctSelectors: 1,
128
+ hotspots: [],
129
+ });
130
+ const text = post.firstCall.args[1].text;
131
+ (0, chai_1.expect)(text).to.include('3 heals across 1 selectors');
132
+ (0, chai_1.expect)(text).to.not.match(/\$|est\./);
133
+ }));
134
+ });
@@ -62,6 +62,8 @@ describe('POST /healing/selector/state — postSelectorStateAction', () => {
62
62
  strategy: 'xpath',
63
63
  selector: '//x',
64
64
  apiKeyId: 'apikey-1',
65
+ userId: null,
66
+ reason: null,
65
67
  });
66
68
  (0, chai_1.expect)(jsonStub.calledOnce).to.be.true;
67
69
  (0, chai_1.expect)(jsonStub.firstCall.args[0].state.status).to.equal('pending');
@@ -137,6 +139,74 @@ describe('POST /healing/selector/state — postSelectorStateAction', () => {
137
139
  (0, chai_1.expect)(statusStub.calledWith(500)).to.be.true;
138
140
  (0, chai_1.expect)(jsonStub.firstCall.args[0]).to.have.property('error', 'internal');
139
141
  }));
142
+ it('passes the mute reason on, trimmed', () => __awaiter(void 0, void 0, void 0, function* () {
143
+ muteStub.resolves({ status: 'muted' });
144
+ const { req, res } = mockReqRes({
145
+ body: {
146
+ original_strategy: 'xpath',
147
+ original_selector: '//x',
148
+ action: 'mute',
149
+ reason: ' Redesign ',
150
+ },
151
+ });
152
+ yield (0, dashboard_1.postSelectorStateAction)(req, res);
153
+ (0, chai_1.expect)(muteStub.firstCall.args[0].reason).to.equal('Redesign');
154
+ }));
155
+ it('keeps a reason only for a mute', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ markFixedStub.resolves({ status: 'pending' });
157
+ const { req, res } = mockReqRes({
158
+ body: {
159
+ original_strategy: 'xpath',
160
+ original_selector: '//x',
161
+ action: 'mark_fixed',
162
+ reason: 'why',
163
+ },
164
+ });
165
+ yield (0, dashboard_1.postSelectorStateAction)(req, res);
166
+ (0, chai_1.expect)(markFixedStub.firstCall.args[0].reason).to.equal(null);
167
+ }));
168
+ it('refuses a reason longer than 500 characters', () => __awaiter(void 0, void 0, void 0, function* () {
169
+ const { req, res, statusStub } = mockReqRes({
170
+ body: {
171
+ original_strategy: 'xpath',
172
+ original_selector: '//x',
173
+ action: 'mute',
174
+ reason: 'x'.repeat(501),
175
+ },
176
+ });
177
+ yield (0, dashboard_1.postSelectorStateAction)(req, res);
178
+ (0, chai_1.expect)(statusStub.calledWith(400)).to.be.true;
179
+ (0, chai_1.expect)(muteStub.called).to.be.false;
180
+ }));
181
+ it('accepts a selector recorded with no strategy', () => __awaiter(void 0, void 0, void 0, function* () {
182
+ markFixedStub.resolves({ status: 'pending' });
183
+ const { req, res } = mockReqRes({
184
+ body: { original_strategy: '', original_selector: '//x', action: 'mark_fixed' },
185
+ });
186
+ yield (0, dashboard_1.postSelectorStateAction)(req, res);
187
+ (0, chai_1.expect)(markFixedStub.firstCall.args[0].strategy).to.equal('');
188
+ }));
189
+ it('records the person acting, not their key', () => __awaiter(void 0, void 0, void 0, function* () {
190
+ markFixedStub.resolves({ status: 'pending' });
191
+ const { req, res } = mockReqRes({
192
+ body: { original_strategy: 'xpath', original_selector: '//x', action: 'mark_fixed' },
193
+ });
194
+ req.auth = { userId: 'u-9', role: 'ADMIN', scopes: 'admin', teamIds: undefined };
195
+ yield (0, dashboard_1.postSelectorStateAction)(req, res);
196
+ (0, chai_1.expect)(markFixedStub.firstCall.args[0].userId).to.equal('u-9');
197
+ }));
198
+ it('answers 500, rather than hanging, when the visibility check fails', () => __awaiter(void 0, void 0, void 0, function* () {
199
+ sinon_1.default.stub(prisma_1.prisma.device, 'findMany').resolves([]);
200
+ sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findFirst').rejects(new Error('database is locked'));
201
+ const { req, res, statusStub, jsonStub } = mockReqRes({
202
+ body: { original_strategy: 'xpath', original_selector: '//x', action: 'mute' },
203
+ });
204
+ req.auth = { userId: 'u-9', role: 'MEMBER', scopes: 'sessions', teamIds: ['team-a'] };
205
+ yield (0, dashboard_1.postSelectorStateAction)(req, res);
206
+ (0, chai_1.expect)(statusStub.calledWith(500)).to.be.true;
207
+ (0, chai_1.expect)(jsonStub.firstCall.args[0]).to.deep.equal({ error: 'internal' });
208
+ (0, chai_1.expect)(muteStub.called).to.be.false;
209
+ }));
140
210
  });
141
211
  describe('GET /healing/state/muted — getMutedSelectors', () => {
142
212
  let findManyStub;
@@ -0,0 +1,118 @@
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 express_1 = __importDefault(require("express"));
19
+ const supertest_1 = __importDefault(require("supertest"));
20
+ const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
21
+ const prisma_1 = require("../../src/prisma");
22
+ const healingTrend_1 = require("../../src/services/selector-health/healingTrend");
23
+ describe('healing trend: days', () => {
24
+ it("reads the browser's offset from tz, and nothing else", () => {
25
+ (0, chai_1.expect)((0, healingTrend_1.parseTzOffset)('330')).to.equal(330);
26
+ (0, chai_1.expect)((0, healingTrend_1.parseTzOffset)('-300')).to.equal(-300);
27
+ (0, chai_1.expect)((0, healingTrend_1.parseTzOffset)('900')).to.equal(0);
28
+ (0, chai_1.expect)((0, healingTrend_1.parseTzOffset)('5.5')).to.equal(0);
29
+ (0, chai_1.expect)((0, healingTrend_1.parseTzOffset)(undefined)).to.equal(0);
30
+ (0, chai_1.expect)((0, healingTrend_1.parseTzOffset)(['330'])).to.equal(0);
31
+ });
32
+ it("starts a day at the caller's midnight", () => {
33
+ // 2 Oct 20:00 UTC is 3 Oct 01:30 in India (+330).
34
+ const t = Date.UTC(2026, 9, 2, 20, 0);
35
+ (0, chai_1.expect)((0, healingTrend_1.localDayStart)(t, 330)).to.equal(Date.UTC(2026, 9, 2, 18, 30));
36
+ (0, chai_1.expect)((0, healingTrend_1.localDayStart)(t, 0)).to.equal(Date.UTC(2026, 9, 2));
37
+ });
38
+ it('counts heals and AI heals per day, with every day of the period present', () => {
39
+ const now = new Date(Date.UTC(2026, 9, 3, 12));
40
+ const since = new Date(now.getTime() - 3 * healingTrend_1.DAY_MS);
41
+ const days = (0, healingTrend_1.dailyHeals)([
42
+ { at: new Date(Date.UTC(2026, 9, 1, 9)), method: 'LLM' },
43
+ { at: new Date(Date.UTC(2026, 9, 1, 10)), method: 'Fuzzy XML' },
44
+ { at: new Date(Date.UTC(2026, 9, 3, 1)), method: 'Visual AI' },
45
+ ], since, now, 0);
46
+ (0, chai_1.expect)(days.map((d) => [new Date(d.t).toISOString().slice(0, 10), d.heals, d.aiHeals])).to.deep.equal([
47
+ ['2026-09-30', 0, 0],
48
+ ['2026-10-01', 2, 1],
49
+ ['2026-10-02', 0, 0],
50
+ ['2026-10-03', 1, 1],
51
+ ]);
52
+ });
53
+ it('puts a heal on the day the caller saw it', () => {
54
+ var _a;
55
+ const now = new Date(Date.UTC(2026, 9, 3, 12));
56
+ const since = new Date(now.getTime() - 2 * healingTrend_1.DAY_MS);
57
+ // 1 Oct 20:00 UTC is 2 Oct 01:30 in India.
58
+ const days = (0, healingTrend_1.dailyHeals)([{ at: new Date(Date.UTC(2026, 9, 1, 20)), method: null }], since, now, 330);
59
+ (0, chai_1.expect)((_a = days.find((d) => d.heals === 1)) === null || _a === void 0 ? void 0 : _a.t).to.equal(Date.UTC(2026, 9, 1, 18, 30));
60
+ });
61
+ });
62
+ describe('GET /healing/summary: time and trend', () => {
63
+ afterEach(() => sinon_1.default.restore());
64
+ function app() {
65
+ const a = (0, express_1.default)();
66
+ a.use((req, _res, next) => {
67
+ req.auth = {
68
+ kind: 'user',
69
+ userId: 'sa',
70
+ role: 'SUPER_ADMIN',
71
+ scopes: 'admin',
72
+ teamIds: undefined,
73
+ };
74
+ next();
75
+ });
76
+ dashboard_1.default.register(a);
77
+ return a;
78
+ }
79
+ it('adds the time spent healing and one trend entry per day of the period', () => __awaiter(void 0, void 0, void 0, function* () {
80
+ const recent = new Date(Date.now() - 60000);
81
+ const findMany = sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findMany');
82
+ findMany.onFirstCall().resolves([
83
+ {
84
+ session_id: 's-1',
85
+ original_selector: '//a',
86
+ healing_tier: 'LLM',
87
+ createdAt: recent,
88
+ duration: 2500,
89
+ },
90
+ {
91
+ session_id: 's-1',
92
+ original_selector: '//a',
93
+ healing_tier: 'Fuzzy XML',
94
+ createdAt: recent,
95
+ duration: null,
96
+ },
97
+ ]);
98
+ findMany.onSecondCall().resolves([
99
+ {
100
+ session_id: 's-0',
101
+ original_selector: '//a',
102
+ healing_tier: 'OCR',
103
+ createdAt: new Date(Date.now() - 9 * healingTrend_1.DAY_MS),
104
+ duration: 4000,
105
+ },
106
+ ]);
107
+ sinon_1.default.stub(prisma_1.prisma.selectorState, 'count').resolves(0);
108
+ const res = yield (0, supertest_1.default)(app()).get('/healing/summary?windowDays=7&tz=0');
109
+ (0, chai_1.expect)(res.status).to.equal(200);
110
+ (0, chai_1.expect)(res.body.current.timeSpentMs).to.equal(2500);
111
+ (0, chai_1.expect)(res.body.prior.timeSpentMs).to.equal(4000);
112
+ (0, chai_1.expect)(res.body.trend).to.have.length(8);
113
+ const sum = (k) => res.body.trend.reduce((s, d) => s + d[k], 0);
114
+ (0, chai_1.expect)([sum('heals'), sum('aiHeals')]).to.deep.equal([2, 1]);
115
+ const select = findMany.firstCall.args[0].select;
116
+ (0, chai_1.expect)(select).to.include({ createdAt: true, duration: true });
117
+ }));
118
+ });
@@ -0,0 +1,47 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
13
+ const chai_1 = require("chai");
14
+ const prisma_1 = require("../../src/prisma");
15
+ const scratch_database_1 = require("../helpers/scratch-database");
16
+ describe('SelectorEvent table', function () {
17
+ this.timeout(90000);
18
+ (0, scratch_database_1.useScratchDatabase)();
19
+ it('keeps who did what to a selector, and why', () => __awaiter(this, void 0, void 0, function* () {
20
+ const at = (s) => new Date(Date.UTC(2026, 9, 2, 10, 0, s));
21
+ yield prisma_1.prisma.selectorEvent.create({
22
+ data: {
23
+ original_strategy: 'xpath',
24
+ original_selector: '//a',
25
+ action: 'muted',
26
+ user_id: 'u-1',
27
+ reason: 'Screen being redesigned',
28
+ createdAt: at(0),
29
+ },
30
+ });
31
+ yield prisma_1.prisma.selectorEvent.create({
32
+ data: {
33
+ original_strategy: 'xpath',
34
+ original_selector: '//a',
35
+ action: 'unmuted',
36
+ createdAt: at(1),
37
+ },
38
+ });
39
+ const rows = yield prisma_1.prisma.selectorEvent.findMany({
40
+ where: { original_strategy: 'xpath', original_selector: '//a' },
41
+ orderBy: { createdAt: 'desc' },
42
+ });
43
+ (0, chai_1.expect)(rows.map((r) => r.action)).to.deep.equal(['unmuted', 'muted']);
44
+ (0, chai_1.expect)(rows[0]).to.include({ user_id: null, reason: null });
45
+ (0, chai_1.expect)(rows[1]).to.include({ user_id: 'u-1', reason: 'Screen being redesigned' });
46
+ }));
47
+ });