@oneuptime/common 14.0.2 → 14.0.3

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 (166) hide show
  1. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  2. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  3. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Project.ts +60 -0
  6. package/Models/DatabaseModels/RumApplication.ts +52 -1
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Services/IncidentAlertService.ts +1811 -0
  11. package/Server/Services/IncidentService.ts +210 -10
  12. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  13. package/Server/Services/Index.ts +2 -0
  14. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  15. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  16. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  17. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  18. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  19. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  20. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  21. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  22. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  23. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  24. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  25. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  26. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  27. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  28. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  29. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  30. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  31. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  32. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  33. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  34. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  35. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  36. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  37. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  38. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  39. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  40. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  41. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  42. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  43. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  44. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  45. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  46. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  47. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  48. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  49. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  50. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  51. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  52. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  53. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  54. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  55. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  56. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  57. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  58. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  59. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  60. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  61. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  62. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  63. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  64. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  65. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  66. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  67. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  68. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  69. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  70. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  71. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  72. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  73. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  74. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  76. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  77. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  78. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  79. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  80. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  81. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  82. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  83. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  84. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  85. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  86. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  87. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  88. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  89. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  90. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  91. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  92. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  93. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  94. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  95. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  96. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  97. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  98. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  99. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  100. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  101. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  102. package/Types/Incident/IncidentAlertLink.ts +38 -0
  103. package/Types/Permission.ts +48 -0
  104. package/Types/Rum/SessionReplay.ts +25 -6
  105. package/Types/Rum/WebVitals.ts +52 -0
  106. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  107. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  108. package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
  109. package/UI/Components/ModelTable/Column.ts +5 -0
  110. package/UI/Components/Table/TableHeader.tsx +47 -2
  111. package/UI/Components/Table/Types/Column.ts +7 -0
  112. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  113. package/UI/Utils/PermissionGate.ts +11 -2
  114. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  115. package/Utils/Rum/SessionTraceState.ts +212 -0
  116. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  117. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  119. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  120. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  121. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  123. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  125. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  127. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  134. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  135. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  136. package/build/dist/Server/Services/IncidentService.js +164 -12
  137. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  138. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  139. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  140. package/build/dist/Server/Services/Index.js +2 -0
  141. package/build/dist/Server/Services/Index.js.map +1 -1
  142. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  143. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  144. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  145. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  146. package/build/dist/Types/Permission.js +42 -0
  147. package/build/dist/Types/Permission.js.map +1 -1
  148. package/build/dist/Types/Rum/WebVitals.js +25 -0
  149. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  150. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  151. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  152. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  153. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  154. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
  155. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  156. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  157. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  158. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  159. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  160. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  161. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  162. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  163. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  164. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  165. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  166. package/package.json +1 -1
@@ -0,0 +1,709 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ HOST_METRIC_DESCRIPTIONS,
6
+ HostMetric,
7
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/HostMetricDescriptions";
8
+ import {
9
+ activeStateMeta,
10
+ computeSystemdAvailability,
11
+ SystemdAvailability,
12
+ unitTypeLabel,
13
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/Utils/SystemdUnits";
14
+ import {
15
+ startupModeLabel,
16
+ statusMeta,
17
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Host/Utils/WindowsServices";
18
+ import {
19
+ expectReadableDescriptionRecord,
20
+ expectTitleExplained,
21
+ MAX_DESCRIPTION_LENGTH,
22
+ } from "./MetricDescriptionRules";
23
+ import AggregationIntervalUtil from "../../../Types/BaseDatabase/AggregationIntervalUtil";
24
+
25
+ /*
26
+ * The (i) texts on the Host pages: the overview, one process, one Windows
27
+ * service, one systemd unit, and the three lists. Each text has to say what
28
+ * the page ACTUALLY computes, so beyond the shared readability rules these
29
+ * pin the facts a customer would otherwise get wrong - which window a tile
30
+ * averages, what a percentage is a share of, where a count is capped - and
31
+ * cross-check the labels a text quotes against the helpers that produce the
32
+ * labels on screen.
33
+ */
34
+
35
+ const HOST_VIEW_DIR: string = path.join(
36
+ __dirname,
37
+ "..",
38
+ "..",
39
+ "..",
40
+ "..",
41
+ "App",
42
+ "FeatureSet",
43
+ "Dashboard",
44
+ "src",
45
+ "Pages",
46
+ "Host",
47
+ "View",
48
+ );
49
+
50
+ function readPage(file: string): string {
51
+ return fs.readFileSync(path.join(HOST_VIEW_DIR, file), "utf8");
52
+ }
53
+
54
+ const D: Record<HostMetric, string> = HOST_METRIC_DESCRIPTIONS;
55
+
56
+ /*
57
+ * Every title a Host page shows beside an (i), and the text it explains
58
+ * with. The wiring suite (App/Tests/Dashboard/HostMetricTooltipWiring)
59
+ * pins that each page really pairs them this way.
60
+ */
61
+ const TITLED: Array<[string, HostMetric]> = [
62
+ ["CPU", "cpu"],
63
+ ["Memory", "memory"],
64
+ ["Filesystem", "filesystem"],
65
+ ["Load avg (1m)", "loadAverage"],
66
+ ["Processes", "processes"],
67
+ ["Availability", "availabilityChart"],
68
+ ["CPU", "cpuChart"],
69
+ ["Memory", "memoryChart"],
70
+ ["Disk space", "diskSpaceChart"],
71
+ ["Network", "networkChart"],
72
+ ["Used / Total", "filesystemUsedTotal"],
73
+ ["Utilization", "filesystemUtilization"],
74
+ ["Process Count (cached)", "processCountCached"],
75
+ ["CPU", "processCpu"],
76
+ ["Memory (RSS)", "processMemoryRss"],
77
+ ["Virtual Memory", "processVirtualMemory"],
78
+ ["Threads", "processThreads"],
79
+ ["CPU", "processCpuChart"],
80
+ ["Memory (RSS)", "processMemoryRssChart"],
81
+ ["Disk I/O", "processDiskIoChart"],
82
+ ["Current Status", "serviceCurrentStatus"],
83
+ ["Availability", "serviceAvailability"],
84
+ ["Startup Mode", "serviceStartupMode"],
85
+ ["State Changes", "serviceStateChanges"],
86
+ ["Status timeline", "serviceStatusTimeline"],
87
+ ["Current State", "unitCurrentState"],
88
+ ["Availability", "unitAvailability"],
89
+ ["Unit Type", "unitType"],
90
+ ["State Changes", "unitStateChanges"],
91
+ ["State timeline", "unitStateTimeline"],
92
+ ["Resources", "hostListResources"],
93
+ ["CPU", "processListCpu"],
94
+ ["Memory", "processListMemory"],
95
+ ["Status", "serviceListStatus"],
96
+ ["Startup", "serviceListStartup"],
97
+ ["State", "unitListState"],
98
+ ["Type", "unitListType"],
99
+ ];
100
+
101
+ const FIVE_MINUTE_TILES: Array<HostMetric> = [
102
+ "cpu",
103
+ "memory",
104
+ "processCpu",
105
+ "processMemoryRss",
106
+ "processVirtualMemory",
107
+ "processThreads",
108
+ ];
109
+
110
+ const PER_INTERVAL_CHARTS: Array<HostMetric> = [
111
+ "cpuChart",
112
+ "memoryChart",
113
+ "diskSpaceChart",
114
+ "processCpuChart",
115
+ "processMemoryRssChart",
116
+ "processDiskIoChart",
117
+ ];
118
+
119
+ const LAST_15_MINUTE_LISTS: Array<HostMetric> = [
120
+ "processListCpu",
121
+ "processListMemory",
122
+ "serviceListStatus",
123
+ "unitListState",
124
+ ];
125
+
126
+ const SPLIT_CPU_READINGS: Array<HostMetric> = [
127
+ "processCpu",
128
+ "processCpuChart",
129
+ "processListCpu",
130
+ ];
131
+
132
+ // Word-boundary matchers, hoisted: eslint's wrap-regex fights prettier.
133
+ const RAW_METRIC_NAME: RegExp = /\b(system|process|windows|systemd)\.[a-z_]+/;
134
+ const PERCENTILE_TOKEN: RegExp = /\bp\d{2}\b/i;
135
+ const RATE_UNIT: RegExp = /bytes per second/i;
136
+ const WHOLE_RANGE: RegExp = /whole[- ]range/;
137
+ const LOGICAL_CORES_FIRST: RegExp = /^Logical CPU cores/;
138
+ const RANGE_OR_AVERAGE: RegExp = /selected range|time range|averag/i;
139
+
140
+ describe("HOST_METRIC_DESCRIPTIONS reads well", () => {
141
+ test("every text passes the shared rules and none repeats another", () => {
142
+ expectReadableDescriptionRecord(D, "HOST_METRIC_DESCRIPTIONS");
143
+ });
144
+
145
+ test("every text stays small - at most 260 characters and two sentences", () => {
146
+ for (const [key, text] of Object.entries(D)) {
147
+ expect({ key, length: text.length }).toEqual({
148
+ key,
149
+ length: Math.min(text.length, 260),
150
+ });
151
+ expect(text.length).toBeLessThanOrEqual(MAX_DESCRIPTION_LENGTH);
152
+
153
+ const sentences: number = text
154
+ .split(/[.!?](?:\s|$)/)
155
+ .filter((part: string): boolean => {
156
+ return part.trim().length > 0;
157
+ }).length;
158
+
159
+ expect({ key, sentences: sentences <= 2 }).toEqual({
160
+ key,
161
+ sentences: true,
162
+ });
163
+ }
164
+ });
165
+
166
+ test.each(TITLED)(
167
+ "%s (%s) is explained by its text",
168
+ (title: string, key: HostMetric) => {
169
+ expectTitleExplained(title, D[key]);
170
+ },
171
+ );
172
+
173
+ test("the title table covers every key exactly once", () => {
174
+ const keys: Array<string> = TITLED.map(
175
+ (entry: [string, HostMetric]): string => {
176
+ return entry[1];
177
+ },
178
+ );
179
+
180
+ expect([...keys].sort()).toEqual(Object.keys(D).sort());
181
+ expect(new Set(keys).size).toBe(keys.length);
182
+ });
183
+
184
+ test("no text names a percentile it would then have to explain", () => {
185
+ for (const text of Object.values(D)) {
186
+ expect(text).not.toMatch(PERCENTILE_TOKEN);
187
+ }
188
+ });
189
+
190
+ test("raw metric names appear only where the customer needs them to act", () => {
191
+ const naming: Array<string> = Object.entries(D)
192
+ .filter((entry: [string, string]): boolean => {
193
+ return RAW_METRIC_NAME.test(entry[1]);
194
+ })
195
+ .map((entry: [string, string]): string => {
196
+ return entry[0];
197
+ });
198
+
199
+ // Threads and open fds are off by default in the collector.
200
+ expect(naming).toEqual(["processThreads"]);
201
+ expect(D.processThreads).toContain("process.threads");
202
+ expect(D.processThreads).toContain("process.open_file_descriptors");
203
+ expect(D.processThreads).toContain("turned on");
204
+ });
205
+
206
+ test("the one text shown as a visible caption is short", () => {
207
+ /*
208
+ * Detail has no tooltip slot, so "Process Count (cached)" shows its
209
+ * text under the label - it has to stay a caption, not a paragraph.
210
+ */
211
+ expect(D.processCountCached.length).toBeLessThanOrEqual(200);
212
+ });
213
+ });
214
+
215
+ describe("time windows are the ones the pages use", () => {
216
+ test.each(FIVE_MINUTE_TILES)(
217
+ "%s says it averages the last 5 minutes of the range",
218
+ (key: HostMetric) => {
219
+ expect(D[key]).toContain("averaged over the last 5 minutes of the range");
220
+ },
221
+ );
222
+
223
+ test("overview CPU and memory admit the whole-range fallback on wide ranges", () => {
224
+ /*
225
+ * meanFromBuckets keeps buckets that START in the last 5 minutes; past
226
+ * 12 hours a bucket is 15 minutes or wider, so usually none does and the
227
+ * tile averages everything.
228
+ */
229
+ for (const key of ["cpu", "memory"] as Array<HostMetric>) {
230
+ expect(D[key]).toContain("12 hours");
231
+ expect(D[key]).toContain("whole-range average");
232
+ }
233
+ });
234
+
235
+ test.each(FIVE_MINUTE_TILES)(
236
+ "%s admits the whole-range fallback when nothing recent arrived",
237
+ (key: HostMetric) => {
238
+ /*
239
+ * The same fallback fires when the last 5 minutes hold no bucket at
240
+ * all - a host gone silent, a process that exited - so the tile shows
241
+ * an old average instead of a dash. The text has to say so.
242
+ */
243
+ expect(D[key]).toContain("12 hours");
244
+ expect(D[key]).toContain("no recent data");
245
+ expect(D[key]).toMatch(WHOLE_RANGE);
246
+ },
247
+ );
248
+
249
+ test.each(FIVE_MINUTE_TILES)(
250
+ "%s says the wide-range fallback is usual, not certain",
251
+ (key: HostMetric) => {
252
+ /*
253
+ * On a range over 12 hours a bucket STARTS inside the 5-minute tile
254
+ * window only when a bucket boundary falls in it: with 15-minute
255
+ * buckets that is a third of the time. So the tile is USUALLY - not
256
+ * always - the whole-range average there.
257
+ */
258
+ expect(D[key]).toContain("usually");
259
+ },
260
+ );
261
+
262
+ test("past 12 hours a bucket is at least three tile windows wide", () => {
263
+ const end: Date = new Date("2026-09-24T12:00:00.000Z");
264
+ const tileWindowMs: number = 5 * 60_000;
265
+
266
+ const bucketMsFor: (hours: number) => number = (hours: number): number => {
267
+ return AggregationIntervalUtil.getAggregationIntervalMs(
268
+ AggregationIntervalUtil.getAggregationIntervalForWindow({
269
+ startDate: new Date(end.getTime() - hours * 3_600_000),
270
+ endDate: end,
271
+ }),
272
+ );
273
+ };
274
+
275
+ // Up to 12 hours a bucket is no wider than the tile window...
276
+ expect(bucketMsFor(3)).toBeLessThanOrEqual(tileWindowMs);
277
+ expect(bucketMsFor(12)).toBeLessThanOrEqual(tileWindowMs);
278
+ // ...past it, a bucket starts in the window a third of the time at most.
279
+ for (const hours of [13, 24, 72, 24 * 7, 24 * 30]) {
280
+ expect(bucketMsFor(hours)).toBeGreaterThanOrEqual(3 * tileWindowMs);
281
+ }
282
+ });
283
+
284
+ test("the pages really fall back to the whole range when no bucket is recent", () => {
285
+ for (const page of ["Overview.tsx", "ProcessView.tsx"]) {
286
+ const source: string = readPage(page);
287
+
288
+ expect(source).toContain("t < tileWindowStartMs");
289
+ expect(source).toContain("if (count === 0) {");
290
+ }
291
+ });
292
+
293
+ test("the filesystem tile says it is a whole-range average, not a 5-minute one", () => {
294
+ expect(D.filesystem).toContain("whole selected range");
295
+ expect(D.filesystem).toContain("not the last 5 minutes");
296
+ expect(D.filesystemUsedTotal).toContain("averaged over the selected range");
297
+ expect(D.filesystemUtilization).toContain(
298
+ "averaged over the selected range",
299
+ );
300
+ });
301
+
302
+ test("load average and processes are the newest interval, not a mean", () => {
303
+ expect(D.loadAverage).toContain("newest interval");
304
+ expect(D.processes).toContain("newest interval");
305
+ expect(D.loadAverage).not.toContain("5 minutes");
306
+ });
307
+
308
+ test.each(PER_INTERVAL_CHARTS)(
309
+ "%s describes one value per interval",
310
+ (key: HostMetric) => {
311
+ expect(D[key]).toContain("each interval");
312
+ },
313
+ );
314
+
315
+ test.each(LAST_15_MINUTE_LISTS)(
316
+ "%s says the list ignores the time picker and reads the last 15 minutes",
317
+ (key: HostMetric) => {
318
+ expect(D[key]).toContain("last 15 minutes");
319
+ expect(D[key]).not.toContain("selected range");
320
+ },
321
+ );
322
+
323
+ test("the lists really look back 15 minutes", () => {
324
+ expect(readPage("Processes.tsx")).toContain(
325
+ "const PROCESS_LOOKBACK_MINUTES: number = 15;",
326
+ );
327
+ expect(readPage("Services.tsx")).toContain(
328
+ "const SERVICE_LOOKBACK_MINUTES: number = 15;",
329
+ );
330
+ expect(readPage("SystemdUnits.tsx")).toContain(
331
+ "const UNIT_LOOKBACK_MINUTES: number = 15;",
332
+ );
333
+ });
334
+
335
+ test("the overview and process tiles really use a 5-minute window", () => {
336
+ for (const page of ["Overview.tsx", "ProcessView.tsx"]) {
337
+ expect(readPage(page)).toContain(
338
+ "const TILE_WINDOW_MINUTES: number = 5;",
339
+ );
340
+ }
341
+ });
342
+ });
343
+
344
+ describe("denominators, units and caps", () => {
345
+ test("CPU texts say 100% is every core, not one core", () => {
346
+ expect(D.cpu).toContain("across all cores");
347
+ expect(D.cpu).toContain("100% means every core was busy");
348
+ expect(D.cpuChart).toContain("100% means every core was busy");
349
+ // The tile and the chart both add user and system time - one wording.
350
+ for (const key of ["cpu", "cpuChart"] as Array<HostMetric>) {
351
+ expect(D[key]).toContain(
352
+ "spent running programs and the operating system",
353
+ );
354
+ }
355
+ expect(D.cpuChart).toContain("(user plus system time)");
356
+ expect(D.cpuChart).not.toContain("busy share");
357
+ expect(D.processCpu).toContain("share of all host CPU cores");
358
+ expect(D.processCpuChart).toContain("total CPU capacity");
359
+ expect(D.processListCpu).toContain("all cores together");
360
+ });
361
+
362
+ test("load average is compared with the core count and can pass 100%", () => {
363
+ expect(D.loadAverage).toContain("number of cores");
364
+ expect(D.loadAverage).toContain("over 100% means work is queuing");
365
+ // The tile's danger threshold is exactly that 100%.
366
+ expect(readPage("Overview.tsx")).toContain(
367
+ "thresholds={{ warn: 70, danger: 100 }}",
368
+ );
369
+ });
370
+
371
+ test("filesystem percentages count reserved space in the total", () => {
372
+ for (const key of [
373
+ "filesystem",
374
+ "diskSpaceChart",
375
+ "filesystemUsedTotal",
376
+ "filesystemUtilization",
377
+ ] as Array<HostMetric>) {
378
+ expect(D[key].toLowerCase()).toContain("reserved");
379
+ }
380
+ expect(D.filesystem).toContain("largest filesystem");
381
+ expect(D.diskSpaceChart).toContain("same mount as the Filesystem tile");
382
+ });
383
+
384
+ test("the filesystem utilization bar colours match the table", () => {
385
+ const overview: string = readPage("Overview.tsx");
386
+
387
+ expect(D.filesystemUtilization).toContain("amber at 75%");
388
+ expect(D.filesystemUtilization).toContain("red at 90%");
389
+ expect(overview).toContain("pct >= 90");
390
+ expect(overview).toContain("pct >= 75");
391
+ });
392
+
393
+ test("sizes are called out as 1024-based, as formatMemoryBytes renders them", () => {
394
+ expect(D.filesystemUsedTotal).toContain("1024-based");
395
+ expect(D.filesystemUsedTotal).toContain("1 GiB is 1,024 MiB");
396
+ expect(readPage("Overview.tsx")).toContain(
397
+ 'const units: Array<string> = ["B", "KiB", "MiB", "GiB", "TiB"];',
398
+ );
399
+ });
400
+
401
+ test("process memory is compared with the host's total RAM", () => {
402
+ expect(D.processMemoryRss).toContain("host's total RAM");
403
+ expect(D.processListMemory).toContain("host's total RAM");
404
+ });
405
+
406
+ test("the process list memory bar colours match memBarColor", () => {
407
+ const processes: string = readPage("Processes.tsx");
408
+
409
+ expect(D.processListMemory).toContain("amber at 10%");
410
+ expect(D.processListMemory).toContain("red at 20%");
411
+ expect(processes).toContain("if (value >= 20) {");
412
+ expect(processes).toContain("if (value >= 10) {");
413
+ });
414
+
415
+ test("network is bytes per second, summed over every interface, loopback included", () => {
416
+ expect(D.networkChart).toMatch(RATE_UNIT);
417
+ expect(D.networkChart).toContain("(In)");
418
+ expect(D.networkChart).toContain("(Out)");
419
+ expect(D.networkChart).toContain("every network interface");
420
+ expect(D.networkChart).toContain("loopback");
421
+ expect(D.processDiskIoChart).toMatch(RATE_UNIT);
422
+ });
423
+
424
+ test("service and unit availability name the 2,000-sample cap the pages fetch", () => {
425
+ for (const key of [
426
+ "serviceAvailability",
427
+ "unitAvailability",
428
+ ] as Array<HostMetric>) {
429
+ expect(D[key]).toContain("newest 2,000 samples");
430
+ expect(D[key]).toContain("capped");
431
+ }
432
+
433
+ expect(readPage("ServiceView.tsx")).toContain(
434
+ "const SAMPLE_FETCH_LIMIT: number = 2000;",
435
+ );
436
+ expect(readPage("SystemdUnitView.tsx")).toContain(
437
+ "const SAMPLE_FETCH_LIMIT: number = 2000;",
438
+ );
439
+ });
440
+
441
+ test("the cached process count says it is not live, and how often it moves", () => {
442
+ /*
443
+ * Host.processCount is written by ingest through ResourceHeartbeat, whose
444
+ * enrichment gate admits one write per 60-second window (plus up to 25%
445
+ * jitter) - "about once a minute", not "every few minutes".
446
+ */
447
+ expect(D.processCountCached).toContain("any state");
448
+ expect(D.processCountCached).toContain("at most about once a minute");
449
+ expect(D.processes).toContain("at most about once a minute");
450
+ expect(D.processes).not.toContain("every few minutes");
451
+ // The total is the tile's sublabel, printed under the number.
452
+ expect(D.processes).toContain("the total on the line below");
453
+ });
454
+
455
+ test("the cached process count's refresh rate is the host heartbeat window", () => {
456
+ const hostService: string = fs.readFileSync(
457
+ path.join(
458
+ __dirname,
459
+ "..",
460
+ "..",
461
+ "..",
462
+ "Server",
463
+ "Services",
464
+ "HostService.ts",
465
+ ),
466
+ "utf8",
467
+ );
468
+
469
+ expect(hostService).toContain(
470
+ "const LAST_SEEN_THROTTLE_SECONDS: number = 60;",
471
+ );
472
+ expect(hostService).toContain("metadata.processCount = extra.processCount");
473
+ expect(hostService).toContain(
474
+ "throttleInSeconds: LAST_SEEN_THROTTLE_SECONDS",
475
+ );
476
+ });
477
+
478
+ test("the Windows fallback of the Processes tile is disclosed", () => {
479
+ expect(D.processes).toContain("Windows");
480
+ expect(D.processes).toContain("every process seen in the last 5 minutes");
481
+ });
482
+
483
+ test("the availability chart explains the uptime badge and the unjudged edge", () => {
484
+ expect(D.availabilityChart).toContain("uptime badge");
485
+ expect(D.availabilityChart).toContain("Up if");
486
+ expect(D.availabilityChart).toContain("Down if");
487
+ expect(D.availabilityChart).toContain("newest interval is not judged");
488
+ });
489
+ });
490
+
491
+ describe("honest about how process CPU is read", () => {
492
+ test.each(SPLIT_CPU_READINGS)(
493
+ "%s says the collector's user, system and wait readings are not added up",
494
+ (key: HostMetric) => {
495
+ expect(D[key]).toContain("user, system and wait");
496
+ },
497
+ );
498
+
499
+ test("the tile and chart say they read about a third of the real use", () => {
500
+ expect(D.processCpu).toContain("about a third of the real use");
501
+ expect(D.processCpuChart).toContain("about a third of the real use");
502
+ });
503
+
504
+ test("the list says it shows only one of the readings", () => {
505
+ expect(D.processListCpu).toContain("only one of them");
506
+ });
507
+
508
+ test("the pages still read process CPU the way the texts describe", () => {
509
+ /*
510
+ * If someone fixes the aggregation (filter or sum the states), these
511
+ * texts must change with it - this test is the reminder.
512
+ */
513
+ const processView: string = readPage("ProcessView.tsx");
514
+ const processes: string = readPage("Processes.tsx");
515
+
516
+ expect(processView).toContain('"process.cpu.utilization",');
517
+ expect(processView).not.toContain('state: "user"');
518
+ expect(processes).toContain('buildQuery("process.cpu.utilization")');
519
+ expect(processes).toContain(
520
+ "// Skip — first datapoint per key is the most recent (sorted DESC).",
521
+ );
522
+ });
523
+ });
524
+
525
+ describe("state texts quote the labels the pages render", () => {
526
+ test("Windows status texts use statusMeta's labels", () => {
527
+ for (const code of [4, 1, 7]) {
528
+ const label: string = statusMeta(code).label;
529
+
530
+ expect(D.serviceCurrentStatus).toContain(label);
531
+ expect(D.serviceListStatus).toContain(label);
532
+ }
533
+ expect(D.serviceAvailability).toContain(statusMeta(4).label);
534
+ });
535
+
536
+ test("Windows status texts cover all four in-progress states", () => {
537
+ /*
538
+ * statusMeta has four pending states: start (2), stop (3), continue (5,
539
+ * resuming a paused service) and pause (6). The texts name each one in
540
+ * plain words rather than quoting "Continue pending".
541
+ */
542
+ expect(
543
+ [2, 3, 5, 6].map((code: number): string => {
544
+ return statusMeta(code).label;
545
+ }),
546
+ ).toEqual([
547
+ "Start pending",
548
+ "Stop pending",
549
+ "Continue pending",
550
+ "Pause pending",
551
+ ]);
552
+
553
+ for (const key of [
554
+ "serviceCurrentStatus",
555
+ "serviceListStatus",
556
+ ] as Array<HostMetric>) {
557
+ expect(D[key]).toContain(
558
+ "a start, stop, pause or resume still in progress",
559
+ );
560
+ }
561
+ });
562
+
563
+ test("startup texts use startupModeLabel's labels", () => {
564
+ for (const mode of [
565
+ "auto_start",
566
+ "demand_start",
567
+ "disabled",
568
+ "boot_start",
569
+ "system_start",
570
+ ]) {
571
+ const label: string = startupModeLabel(mode);
572
+
573
+ expect(D.serviceStartupMode).toContain(label);
574
+ expect(D.serviceListStartup).toContain(label);
575
+ }
576
+ });
577
+
578
+ test("systemd state texts use activeStateMeta's labels", () => {
579
+ for (const state of ["active", "inactive", "failed", "activating"]) {
580
+ expect(D.unitCurrentState).toContain(activeStateMeta(state).label);
581
+ }
582
+ for (const state of ["active", "failed"]) {
583
+ expect(D.unitListState).toContain(activeStateMeta(state).label);
584
+ }
585
+ expect(D.unitListState).toContain(activeStateMeta(null).label);
586
+ });
587
+
588
+ test("unit type texts use unitTypeLabel's labels", () => {
589
+ expect(D.unitType).toContain(unitTypeLabel("nginx.service"));
590
+ expect(D.unitType).toContain(unitTypeLabel("docker.socket"));
591
+ expect(D.unitType).toContain(unitTypeLabel("logrotate.timer"));
592
+ expect(D.unitListType).toContain(unitTypeLabel("nginx.service"));
593
+ expect(D.unitListType).toContain(unitTypeLabel("logrotate.timer"));
594
+ });
595
+
596
+ test("unit availability counts Reloading as not active, as the text says", () => {
597
+ const availability: SystemdAvailability = computeSystemdAvailability([
598
+ { time: new Date(0), state: "active" },
599
+ { time: new Date(30_000), state: "reloading" },
600
+ { time: new Date(60_000), state: "active" },
601
+ { time: new Date(90_000), state: "refreshing" },
602
+ ]);
603
+
604
+ expect(availability.percent).toBe(50);
605
+ expect(D.unitAvailability).toContain("Reloading counts as not active");
606
+ expect(D.unitAvailability).toContain("Active");
607
+ });
608
+
609
+ test("timelines say they show the worst state, as the Min x-axis does", () => {
610
+ expect(D.serviceStatusTimeline).toContain("worst status");
611
+ expect(D.unitStateTimeline).toContain("worst state");
612
+ expect(readPage("ServiceView.tsx")).toContain(
613
+ "aggregateType: XAxisAggregateType.Min",
614
+ );
615
+ expect(readPage("SystemdUnitView.tsx")).toContain(
616
+ "aggregateType: XAxisAggregateType.Min",
617
+ );
618
+ });
619
+
620
+ test("state-change counts admit what sampling cannot see", () => {
621
+ expect(D.serviceStateChanges).toContain("between two samples is not seen");
622
+ expect(D.unitStateChanges).toContain("between two samples is not seen");
623
+ });
624
+ });
625
+
626
+ describe("the Hosts list Resources column reads cached host columns", () => {
627
+ /*
628
+ * The column shows Host.cpuCores, Host.totalMemoryBytes and
629
+ * Host.processCount - values ingest saves on the host record while it
630
+ * scans each metrics batch, not telemetry fetched for a range. Each claim
631
+ * of the text is pinned to the code that fills the column.
632
+ */
633
+ const REPO_PACKAGES: string = path.join(__dirname, "..", "..", "..", "..");
634
+ const ingestBase: string = fs.readFileSync(
635
+ path.join(
636
+ REPO_PACKAGES,
637
+ "App",
638
+ "FeatureSet",
639
+ "Telemetry",
640
+ "Services",
641
+ "OtelIngestBaseService.ts",
642
+ ),
643
+ "utf8",
644
+ );
645
+ const hostService: string = fs.readFileSync(
646
+ path.join(REPO_PACKAGES, "Common", "Server", "Services", "HostService.ts"),
647
+ "utf8",
648
+ );
649
+ const hostDocs: string = fs.readFileSync(
650
+ path.join(HOST_VIEW_DIR, "..", "Utils", "DocumentationMarkdown.ts"),
651
+ "utf8",
652
+ );
653
+
654
+ // The branch of scanHostInfraStatsFromMetrics that handles one metric.
655
+ function scanBranch(metricName: string): string {
656
+ const start: number = ingestBase.indexOf(`name === "${metricName}"`);
657
+
658
+ expect(start).toBeGreaterThan(-1);
659
+
660
+ return ingestBase.slice(start, ingestBase.indexOf("continue;", start));
661
+ }
662
+
663
+ test("cores are the logical CPU count, so hyperthreads count", () => {
664
+ const cores: string = scanBranch("system.cpu.logical.count");
665
+
666
+ expect(cores).toContain("this.firstDatapointNumber(m)");
667
+ expect(cores).toContain("result.cpuCores = Math.round(v);");
668
+ // The setup guide turns the (off-by-default) logical count on.
669
+ expect(hostDocs).toContain("system.cpu.logical.count:");
670
+ expect(D.hostListResources).toMatch(LOGICAL_CORES_FIRST);
671
+ expect(D.hostListResources).toContain("each hyperthread counts as one");
672
+ });
673
+
674
+ test("total RAM is every memory state added up, and the text says so", () => {
675
+ const memory: string = scanBranch("system.memory.usage");
676
+
677
+ expect(memory).toContain("this.sumDatapointNumbers(m)");
678
+ expect(memory).toContain("result.totalMemoryBytes = Math.round(v);");
679
+ expect(D.hostListResources).toContain(
680
+ "the sum of every memory state the collector reports",
681
+ );
682
+ });
683
+
684
+ test("processes are summed over every status, as 'in any state' says", () => {
685
+ const processes: string = scanBranch("system.processes.count");
686
+
687
+ expect(processes).toContain("this.sumDatapointNumbers(m)");
688
+ expect(processes).toContain("result.processCount = Math.round(v);");
689
+ expect(D.hostListResources).toContain("processes in any state");
690
+ // The same number as the overview's cached count, in the same words.
691
+ expect(D.processCountCached).toContain("any state");
692
+ });
693
+
694
+ test("all three ride the host heartbeat, so 'at most about once a minute'", () => {
695
+ expect(hostService).toContain(
696
+ "const LAST_SEEN_THROTTLE_SECONDS: number = 60;",
697
+ );
698
+ for (const field of ["cpuCores", "totalMemoryBytes", "processCount"]) {
699
+ expect(hostService).toContain(`metadata.${field} = extra.${field};`);
700
+ }
701
+ expect(D.hostListResources).toContain(
702
+ "latest values saved on the host, updated at most about once a minute",
703
+ );
704
+ });
705
+
706
+ test("the text claims no time range or average - the list has no time picker", () => {
707
+ expect(D.hostListResources).not.toMatch(RANGE_OR_AVERAGE);
708
+ });
709
+ });