@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,801 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import {
5
+ VMWARE_METRIC_DESCRIPTIONS,
6
+ VMwareMetric,
7
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/VMwareMetricDescriptions";
8
+ import {
9
+ expectReadableDescriptionRecord,
10
+ expectTitleExplained,
11
+ metricDescriptionProblems,
12
+ } from "./MetricDescriptionRules";
13
+ import AggregationIntervalUtil from "../../../Types/BaseDatabase/AggregationIntervalUtil";
14
+
15
+ /*
16
+ * The (i) texts on the VMware (vCenter) pages. The shared rules keep them
17
+ * short and finished; the anchors below keep them TRUE. Each anchor reads
18
+ * the constant or the line of code a sentence depends on - the tile window,
19
+ * the staleness cut-off, the datastore and CPU Ready thresholds, the
20
+ * forecast window - so changing the computation without changing the words
21
+ * fails here instead of quietly misleading a customer.
22
+ */
23
+
24
+ const DASHBOARD_SRC: string = path.join(
25
+ __dirname,
26
+ "..",
27
+ "..",
28
+ "..",
29
+ "..",
30
+ "App",
31
+ "FeatureSet",
32
+ "Dashboard",
33
+ "src",
34
+ );
35
+
36
+ const COMMON_ROOT: string = path.join(__dirname, "..", "..", "..");
37
+
38
+ function readDashboard(relativePath: string): string {
39
+ return fs.readFileSync(
40
+ path.join(DASHBOARD_SRC, ...relativePath.split("/")),
41
+ "utf8",
42
+ );
43
+ }
44
+
45
+ const OVERVIEW: string = readDashboard("Pages/VMware/View/Index.tsx");
46
+ const DATASTORE_DETAIL: string = readDashboard(
47
+ "Pages/VMware/View/DatastoreDetail.tsx",
48
+ );
49
+ const VM_DETAIL: string = readDashboard(
50
+ "Pages/VMware/View/VirtualMachineDetail.tsx",
51
+ );
52
+ const RESOURCE_POOLS: string = readDashboard(
53
+ "Pages/VMware/View/ResourcePools.tsx",
54
+ );
55
+ const UTILS: string = readDashboard(
56
+ "Pages/VMware/Utils/VMwareResourceUtils.ts",
57
+ );
58
+ const INVENTORY_SERVICE: string = fs.readFileSync(
59
+ path.join(COMMON_ROOT, "Server", "Services", "VMwareResourceService.ts"),
60
+ "utf8",
61
+ );
62
+
63
+ const WHITESPACE: RegExp = /\s+/g;
64
+
65
+ function squash(source: string): string {
66
+ return source.replace(WHITESPACE, " ");
67
+ }
68
+
69
+ /*
70
+ * A `const NAME: number = <expr>;` in a page, evaluated. The expressions are
71
+ * whole numbers or products of them (`15 * 60 * 1000`); anything else fails
72
+ * the test rather than being guessed at.
73
+ */
74
+ const PRODUCT_OF_INTEGERS: RegExp = /^\d+(\s*\*\s*\d+)*$/;
75
+
76
+ function numericConstant(source: string, name: string): number {
77
+ const declaration: RegExp = new RegExp(`const ${name}: number =\\s*([^;]+);`);
78
+ const match: RegExpMatchArray | null = source.match(declaration);
79
+
80
+ expect({ name, found: Boolean(match) }).toEqual({ name, found: true });
81
+
82
+ const expression: string = (match as RegExpMatchArray)[1]!.trim();
83
+
84
+ expect(expression).toMatch(PRODUCT_OF_INTEGERS);
85
+
86
+ return expression.split("*").reduce((product: number, factor: string) => {
87
+ return product * Number(factor.trim());
88
+ }, 1);
89
+ }
90
+
91
+ const D: Record<VMwareMetric, string> = VMWARE_METRIC_DESCRIPTIONS;
92
+
93
+ /*
94
+ * The visible title beside each (i), as the page renders it. Used to check a
95
+ * title that names jargon gets a text that explains it.
96
+ */
97
+ const TITLES: Record<VMwareMetric, string> = {
98
+ overviewInventoryCounts: "vCenter inventory counts",
99
+ overviewHostEffectiveness: "Host Effectiveness",
100
+ overviewHostCpu: "Host CPU",
101
+ overviewHostMemory: "Host Memory",
102
+ overviewDatastores: "Datastores",
103
+ overviewVmCpuReady: "VM CPU Ready",
104
+ overviewVirtualMachines: "Virtual Machines",
105
+ overviewHostCpuChart: "Host CPU",
106
+ overviewHostMemoryChart: "Host Memory",
107
+ overviewDatastoreUsedChart: "Datastore Used",
108
+ overviewVmCpuReadyChart: "VM CPU Ready",
109
+ overviewVCenterHealth: "vCenter Health",
110
+ overviewDatacenterCount: "Datacenters",
111
+ overviewClusterCount: "Clusters",
112
+ overviewHostCount: "Hosts",
113
+ overviewVirtualMachineCount: "Virtual Machines",
114
+ overviewDatastoreCount: "Datastores",
115
+ overviewResourcePoolCount: "Resource Pools",
116
+ overviewAgentStatus: "Agent Status",
117
+ topHostsByCpu: "Host CPU",
118
+ topHostsByMemory: "Host Memory",
119
+ topDatastoresByUtilization: "Datastore Utilization",
120
+ topVmsByCpuReady: "VM CPU Ready",
121
+ hostCpu: "CPU",
122
+ hostCpuCapacity: "CPU Capacity",
123
+ hostMemory: "Memory (Used / Capacity)",
124
+ vmPowerState: "Power State",
125
+ vmCpu: "CPU",
126
+ vmCpuReady: "CPU Ready",
127
+ vmMemory: "Memory (Used / Configured)",
128
+ vmMemoryBallooned: "Memory Ballooned",
129
+ vmMemorySwapped: "Memory Swapped",
130
+ vmDisk: "Disk (Used / Provisioned)",
131
+ datastoreUsed: "Used",
132
+ datastoreCapacity: "Capacity",
133
+ datastoreFree: "Free",
134
+ datastoreUsedPercent: "Used %",
135
+ datastoreGrowthForecast: "Growth Forecast",
136
+ clusterHosts: "Hosts",
137
+ clusterVirtualMachines: "Virtual Machines",
138
+ clusterTemplates: "VM Templates",
139
+ clusterCpu: "CPU (Effective / Total)",
140
+ clusterMemory: "Memory (Effective / Total)",
141
+ hostListCpu: "CPU",
142
+ hostListMemory: "Memory",
143
+ hostListCpuCapacity: "CPU Capacity",
144
+ vmListStatus: "Status",
145
+ vmListPowerState: "Power State",
146
+ vmListCpuReady: "CPU Ready",
147
+ vmListCpu: "CPU",
148
+ vmListMemory: "Memory",
149
+ vmListAge: "Age",
150
+ datastoreListUsedCapacity: "Used / Capacity",
151
+ clusterListHosts: "Hosts (effective / total)",
152
+ clusterListVirtualMachines: "VMs (powered on / total)",
153
+ clusterListTemplates: "Templates",
154
+ clusterListEffectiveCpu: "Effective CPU",
155
+ clusterListEffectiveMemory: "Effective Memory",
156
+ resourcePoolListCpu: "CPU Usage",
157
+ resourcePoolListMemory: "Memory Usage",
158
+ resourcePoolListBalloonedSwapped: "Ballooned / Swapped",
159
+ };
160
+
161
+ const ALL_KEYS: Array<VMwareMetric> = Object.keys(D) as Array<VMwareMetric>;
162
+
163
+ const SENTENCE_BREAK: RegExp = /[.!?]\s+(?=[A-Z0-9])/g;
164
+ // A raw receiver metric name such as vcenter.vm.cpu.readiness.
165
+ const RAW_METRIC_NAME: RegExp = /\bvcenter\.[a-z_]+\.[a-z_.]+/i;
166
+ const MARKETING_WORDS: RegExp =
167
+ /\b(powerful|seamless|blazing|world-class|best-in-class|effortless)\b/i;
168
+ const LIVE_CLAIM: RegExp = /\b(right now|real[- ]time|live)\b/i;
169
+
170
+ function sentenceCount(text: string): number {
171
+ return (text.match(SENTENCE_BREAK) || []).length + 1;
172
+ }
173
+
174
+ describe("VMware metric descriptions: shape", () => {
175
+ test("every description passes the shared rules and none repeats another", () => {
176
+ expectReadableDescriptionRecord(D, "VMWARE_METRIC_DESCRIPTIONS");
177
+ });
178
+
179
+ test("the record and the title table cover the same metrics", () => {
180
+ expect(Object.keys(TITLES).sort()).toEqual([...ALL_KEYS].sort());
181
+ });
182
+
183
+ test.each(ALL_KEYS)("%s is one or two sentences", (key: VMwareMetric) => {
184
+ expect({ key, sentences: sentenceCount(D[key]) <= 2 }).toEqual({
185
+ key,
186
+ sentences: true,
187
+ });
188
+ });
189
+
190
+ test.each(ALL_KEYS)(
191
+ "%s fits a small tooltip (at most 300 characters)",
192
+ (key: VMwareMetric) => {
193
+ expect(D[key].length).toBeLessThanOrEqual(300);
194
+ expect(metricDescriptionProblems(D[key])).toEqual([]);
195
+ },
196
+ );
197
+
198
+ test.each(ALL_KEYS)(
199
+ "%s explains any percentile its title names",
200
+ (key: VMwareMetric) => {
201
+ expectTitleExplained(TITLES[key], D[key]);
202
+ },
203
+ );
204
+
205
+ test.each(ALL_KEYS)(
206
+ "%s uses plain words, not raw receiver metric names or marketing",
207
+ (key: VMwareMetric) => {
208
+ expect(D[key]).not.toMatch(RAW_METRIC_NAME);
209
+ expect(D[key]).not.toMatch(MARKETING_WORDS);
210
+ expect(D[key]).not.toContain("DRS");
211
+ expect(D[key]).not.toContain("QuickStats");
212
+ },
213
+ );
214
+
215
+ test("a title shown more than once on the overview gets a different text each time", () => {
216
+ // Host CPU is a tile, a chart and a top-5 list - three different numbers.
217
+ expect(
218
+ new Set([D.overviewHostCpu, D.overviewHostCpuChart, D.topHostsByCpu])
219
+ .size,
220
+ ).toBe(3);
221
+ expect(
222
+ new Set([
223
+ D.overviewHostMemory,
224
+ D.overviewHostMemoryChart,
225
+ D.topHostsByMemory,
226
+ ]).size,
227
+ ).toBe(3);
228
+ expect(
229
+ new Set([
230
+ D.overviewVmCpuReady,
231
+ D.overviewVmCpuReadyChart,
232
+ D.topVmsByCpuReady,
233
+ ]).size,
234
+ ).toBe(3);
235
+ expect(D.overviewDatastores).not.toBe(D.overviewDatastoreCount);
236
+ expect(D.overviewVirtualMachines).not.toBe(D.overviewVirtualMachineCount);
237
+ });
238
+ });
239
+
240
+ describe("VMware metric descriptions: the time window each number covers", () => {
241
+ const tileWindowMinutes: number = numericConstant(
242
+ OVERVIEW,
243
+ "TILE_WINDOW_MINUTES",
244
+ );
245
+
246
+ test("the overview still averages its tiles over a 5-minute window", () => {
247
+ expect(tileWindowMinutes).toBe(5);
248
+ expect(OVERVIEW).toContain("-TILE_WINDOW_MINUTES,");
249
+ });
250
+
251
+ test.each([
252
+ "overviewHostCpu",
253
+ "overviewHostMemory",
254
+ "overviewVmCpuReady",
255
+ ] as Array<VMwareMetric>)(
256
+ "%s says it averages only the last minutes of the selected range",
257
+ (key: VMwareMetric) => {
258
+ expect(D[key]).toContain(
259
+ `last ${tileWindowMinutes} minutes of the selected range`,
260
+ );
261
+ },
262
+ );
263
+
264
+ test.each([
265
+ "overviewHostCpu",
266
+ "overviewHostMemory",
267
+ "overviewVmCpuReady",
268
+ ] as Array<VMwareMetric>)(
269
+ "%s names the whole-range fallback the code takes, and when it is usual",
270
+ (key: VMwareMetric) => {
271
+ // meanInRecentWindow falls back to every point when the window is empty.
272
+ expect(OVERVIEW).toContain("if (count === 0) {");
273
+ /*
274
+ * The window is empty not only when data stops, but whenever no bucket
275
+ * STARTS in it - which past 12 hours is most of the time (below). The
276
+ * same words as the Kubernetes, Proxmox and container host tiles.
277
+ */
278
+ expect(D[key]).toContain(
279
+ "(often the whole range on ranges over 12 hours or without recent data)",
280
+ );
281
+ expect(D[key]).not.toContain("if those minutes have no data");
282
+ },
283
+ );
284
+
285
+ test("past 12 hours a bucket is wider than the tile window, so one seldom starts in it", () => {
286
+ // The overview never pins a bucket size, so the window-derived one applies.
287
+ expect(OVERVIEW).not.toContain("aggregationInterval");
288
+
289
+ const end: Date = new Date("2026-09-24T12:00:00.000Z");
290
+ const tileWindowMs: number = tileWindowMinutes * 60_000;
291
+ const bucketMsFor: (hours: number) => number = (hours: number): number => {
292
+ return AggregationIntervalUtil.getAggregationIntervalMs(
293
+ AggregationIntervalUtil.getAggregationIntervalForWindow({
294
+ startDate: new Date(end.getTime() - hours * 3_600_000),
295
+ endDate: end,
296
+ }),
297
+ );
298
+ };
299
+
300
+ expect(bucketMsFor(12)).toBeLessThanOrEqual(tileWindowMs);
301
+ for (const hours of [13, 24, 72, 24 * 7, 24 * 30]) {
302
+ expect(bucketMsFor(hours)).toBeGreaterThanOrEqual(3 * tileWindowMs);
303
+ }
304
+ });
305
+
306
+ test("the agent status names the disconnect delay the server applies", () => {
307
+ /*
308
+ * A vCenter is marked disconnected once lastSeenAt is 15 minutes old, by
309
+ * a job that runs every 5 minutes - the words Proxmox and Kubernetes use.
310
+ */
311
+ const service: string = fs.readFileSync(
312
+ path.join(COMMON_ROOT, "Server", "Services", "VMwareVCenterService.ts"),
313
+ "utf8",
314
+ );
315
+ const job: string = fs.readFileSync(
316
+ path.join(
317
+ COMMON_ROOT,
318
+ "..",
319
+ "App",
320
+ "FeatureSet",
321
+ "Workers",
322
+ "Jobs",
323
+ "VMware",
324
+ "CleanupStaleResources.ts",
325
+ ),
326
+ "utf8",
327
+ );
328
+ const disconnect: string = service.slice(
329
+ service.indexOf("public async markDisconnectedVCenters()"),
330
+ );
331
+
332
+ expect(disconnect.replace(WHITESPACE, " ")).toContain(
333
+ "OneUptimeDate.getCurrentDate(), -15, );",
334
+ );
335
+ expect(job).toContain("schedule: EVERY_FIVE_MINUTE");
336
+ expect(D.overviewAgentStatus).toContain(
337
+ "about 15 to 20 minutes after data stops arriving",
338
+ );
339
+ });
340
+
341
+ test("every recent-window tile really goes through meanInRecentWindow", () => {
342
+ for (const stat of [
343
+ "hostCpuPercent: meanInRecentWindow(cpuPoints)",
344
+ "hostMemoryPercent: meanInRecentWindow(memoryPercentPoints)",
345
+ "vmCpuReadyAvgPercent: meanInRecentWindow(cpuReadyAvgPoints)",
346
+ "vmCpuReadyMaxPercent: meanInRecentWindow(cpuReadyMaxPoints)",
347
+ "datastoreWorstPercent: meanInRecentWindow(datastoreWorstPoints)",
348
+ ]) {
349
+ expect({ stat, found: OVERVIEW.includes(stat) }).toEqual({
350
+ stat,
351
+ found: true,
352
+ });
353
+ }
354
+ });
355
+
356
+ test("the VM CPU Ready sublabel is the busiest VM, not a second average", () => {
357
+ // Max per point across VMs, then the recent-window mean of those maxima.
358
+ expect(OVERVIEW).toContain("max = Math.max(max, v);");
359
+ expect(OVERVIEW).toContain(
360
+ "`max ${formatPercent(s.vmCpuReadyMaxPercent)} across",
361
+ );
362
+ expect(D.overviewVmCpuReady).toContain("busiest VM below");
363
+ expect(D.overviewVmCpuReady).not.toContain("highest single VM");
364
+ });
365
+
366
+ test("the Datastores tile names its metric fallback window", () => {
367
+ expect(OVERVIEW).toContain(
368
+ "inventory?.worstDatastorePercent ?? s.datastoreWorstPercent",
369
+ );
370
+ expect(D.overviewDatastores).toContain(
371
+ `until the inventory loads, the fullest over the last ${tileWindowMinutes} minutes of the range`,
372
+ );
373
+ });
374
+
375
+ test("the Datastores tile reads every datastore's last report, fresh or not", () => {
376
+ // The Datastore branch of loadInventory never consults `fresh`.
377
+ const branch: string = OVERVIEW.slice(
378
+ OVERVIEW.indexOf(
379
+ "} else if (row.kind === VMwareResourceKind.Datastore) {",
380
+ ),
381
+ OVERVIEW.indexOf(
382
+ "} else if (row.kind === VMwareResourceKind.ResourcePool) {",
383
+ ),
384
+ );
385
+
386
+ expect(branch).toContain("toNumber(row.latestDiskBytes)");
387
+ expect(branch).not.toContain("fresh");
388
+ expect(D.overviewDatastores).toContain("latest report, however old");
389
+ });
390
+
391
+ test.each([
392
+ "overviewHostCpuChart",
393
+ "overviewHostMemoryChart",
394
+ "overviewDatastoreUsedChart",
395
+ "overviewVmCpuReadyChart",
396
+ ] as Array<VMwareMetric>)(
397
+ "%s covers the whole selected range, not the tile window",
398
+ (key: VMwareMetric) => {
399
+ expect(D[key]).toContain("selected range");
400
+ expect(D[key]).not.toContain("minutes");
401
+ },
402
+ );
403
+
404
+ test.each([
405
+ "overviewInventoryCounts",
406
+ "overviewHostEffectiveness",
407
+ "overviewVirtualMachines",
408
+ "overviewDatacenterCount",
409
+ "overviewHostCount",
410
+ "overviewVirtualMachineCount",
411
+ "topHostsByCpu",
412
+ "topHostsByMemory",
413
+ "topVmsByCpuReady",
414
+ ] as Array<VMwareMetric>)(
415
+ "%s is read from the latest inventory, not the time picker",
416
+ (key: VMwareMetric) => {
417
+ expect(D[key]).toContain("latest inventory");
418
+ expect(D[key]).not.toContain(`last ${tileWindowMinutes} minutes`);
419
+ },
420
+ );
421
+
422
+ test("the inventory tiles that sit beside time-ranged ones say they ignore the range", () => {
423
+ expect(D.overviewInventoryCounts).toContain(
424
+ "rather than the selected range",
425
+ );
426
+ expect(D.overviewHostEffectiveness).toContain(
427
+ "rather than the selected range",
428
+ );
429
+ expect(D.topHostsByCpu).toContain("rather than the selected range");
430
+ expect(D.topDatastoresByUtilization).toContain("not the selected range");
431
+ });
432
+
433
+ test.each(ALL_KEYS)(
434
+ "%s does not promise a live value: every number is a stored report",
435
+ (key: VMwareMetric) => {
436
+ /*
437
+ * Nothing on these pages is read live from vCenter; inventory values
438
+ * can be old (the cluster columns are never staleness-checked), so
439
+ * "right now" would overstate them.
440
+ */
441
+ expect(D[key]).not.toMatch(LIVE_CLAIM);
442
+ },
443
+ );
444
+ });
445
+
446
+ describe("VMware metric descriptions: stale data", () => {
447
+ const staleMs: number = numericConstant(UTILS, "METRIC_STALE_MS");
448
+ const staleMinutes: number = staleMs / 60000;
449
+
450
+ test("the inventory cut-off is still 15 minutes", () => {
451
+ expect(staleMinutes).toBe(15);
452
+ });
453
+
454
+ test("the tables gate CPU and memory - and only those - on fresh metrics", () => {
455
+ const mapping: string = UTILS.slice(
456
+ UTILS.indexOf("export function toInfrastructureResource"),
457
+ );
458
+
459
+ expect(mapping).toContain("if (hasFreshMetrics(row)) {");
460
+ expect(mapping).toContain("cpu = toNumberOrNull(row.latestCpuPercent);");
461
+ expect(mapping).toContain("mem = toNumberOrNull(row.latestMemoryBytes);");
462
+ // The rest ride additionalAttributes verbatim, however old.
463
+ expect(mapping).toContain('additionalAttributes["cpuReadinessPercent"]');
464
+ expect(mapping).toContain('additionalAttributes["diskBytes"]');
465
+ expect(mapping).toContain('additionalAttributes["cpuMhz"]');
466
+ });
467
+
468
+ test.each([
469
+ "hostListCpu",
470
+ "hostListMemory",
471
+ "vmListCpu",
472
+ "vmListMemory",
473
+ "resourcePoolListMemory",
474
+ "topHostsByCpu",
475
+ "topHostsByMemory",
476
+ "topVmsByCpuReady",
477
+ ] as Array<VMwareMetric>)(
478
+ "%s says values older than the cut-off are hidden",
479
+ (key: VMwareMetric) => {
480
+ expect(D[key]).toContain(`last ${staleMinutes} minutes`);
481
+ },
482
+ );
483
+
484
+ test.each([
485
+ "vmListCpuReady",
486
+ "datastoreListUsedCapacity",
487
+ "resourcePoolListCpu",
488
+ "topDatastoresByUtilization",
489
+ "hostCpu",
490
+ ] as Array<VMwareMetric>)(
491
+ "%s shows the last value however old, and says so",
492
+ (key: VMwareMetric) => {
493
+ expect(D[key]).toContain("however old");
494
+ expect(D[key]).not.toContain(`${staleMinutes} minutes`);
495
+ },
496
+ );
497
+
498
+ test("the ballooned / swapped column is not staleness-checked either", () => {
499
+ expect(squash(RESOURCE_POOLS)).toContain(
500
+ 'readNumber( resource, "memoryBalloonedBytes", )',
501
+ );
502
+ expect(D.resourcePoolListBalloonedSwapped).toContain("last report");
503
+ expect(D.resourcePoolListBalloonedSwapped).not.toContain("15 minutes");
504
+ });
505
+
506
+ test("a powered-off VM keeps its last CPU reading, and the texts say so", () => {
507
+ // The inventory mirror keeps the previous value when a batch has no CPU.
508
+ expect(INVENTORY_SERVICE).toContain(
509
+ '"latestCpuPercent" = COALESCE(v."cpu", p."latestCpuPercent")',
510
+ );
511
+ expect(INVENTORY_SERVICE).toContain(
512
+ '"cpuReadinessPercent" = COALESCE(v."cpuReady", p."cpuReadinessPercent")',
513
+ );
514
+
515
+ for (const key of [
516
+ "vmCpu",
517
+ "vmCpuReady",
518
+ "vmListCpu",
519
+ "vmListCpuReady",
520
+ ] as Array<VMwareMetric>) {
521
+ expect({ key, says: D[key].includes("last reading") }).toEqual({
522
+ key,
523
+ says: true,
524
+ });
525
+ }
526
+ });
527
+ });
528
+
529
+ describe("VMware metric descriptions: thresholds match the code", () => {
530
+ const warn: number = numericConstant(OVERVIEW, "DATASTORE_WARN_PERCENT");
531
+ const critical: number = numericConstant(
532
+ OVERVIEW,
533
+ "DATASTORE_CRITICAL_PERCENT",
534
+ );
535
+ const cpuReady: number = numericConstant(
536
+ OVERVIEW,
537
+ "VM_CPU_READY_WARN_PERCENT",
538
+ );
539
+
540
+ test("the datastore detail page flags at the same levels as the overview", () => {
541
+ expect(numericConstant(DATASTORE_DETAIL, "DATASTORE_WARN_PERCENT")).toBe(
542
+ warn,
543
+ );
544
+ expect(
545
+ numericConstant(DATASTORE_DETAIL, "DATASTORE_CRITICAL_PERCENT"),
546
+ ).toBe(critical);
547
+ });
548
+
549
+ test.each([
550
+ "overviewDatastores",
551
+ "overviewDatastoreCount",
552
+ "overviewVCenterHealth",
553
+ "datastoreUsedPercent",
554
+ ] as Array<VMwareMetric>)(
555
+ "%s names the datastore warning and critical levels",
556
+ (key: VMwareMetric) => {
557
+ expect(D[key]).toContain(`${warn}%`);
558
+ expect(D[key]).toContain(`${critical}%`);
559
+ },
560
+ );
561
+
562
+ test.each([
563
+ "overviewVmCpuReady",
564
+ "overviewVmCpuReadyChart",
565
+ "overviewVCenterHealth",
566
+ "topVmsByCpuReady",
567
+ "vmCpuReady",
568
+ "vmListCpuReady",
569
+ ] as Array<VMwareMetric>)(
570
+ "%s names the CPU Ready level that counts as contention",
571
+ (key: VMwareMetric) => {
572
+ expect(D[key]).toContain(`${cpuReady}%`);
573
+ },
574
+ );
575
+
576
+ test("the VM detail page flags CPU Ready at the same level", () => {
577
+ expect(VM_DETAIL).toContain(`readiness > ${cpuReady} ?`);
578
+ });
579
+
580
+ test("the VM CPU Ready tile turns amber where its text says", () => {
581
+ expect(OVERVIEW).toContain(
582
+ "thresholds={{ warn: 5, danger: VM_CPU_READY_WARN_PERCENT }}",
583
+ );
584
+ expect(D.overviewVmCpuReady).toContain("amber from 5%");
585
+ expect(D.overviewVmCpuReady).toContain(`red from ${cpuReady}%`);
586
+ });
587
+
588
+ test("the datastore tile turns amber and red where its text says", () => {
589
+ expect(OVERVIEW).toContain("warn: DATASTORE_WARN_PERCENT,");
590
+ expect(OVERVIEW).toContain("danger: DATASTORE_CRITICAL_PERCENT,");
591
+ expect(D.overviewDatastores).toContain(`Amber from ${warn}%`);
592
+ expect(D.overviewDatastores).toContain(`red from ${critical}%`);
593
+ });
594
+
595
+ test("the growth forecast names its window and cut-off", () => {
596
+ const hours: number = numericConstant(
597
+ DATASTORE_DETAIL,
598
+ "PROJECTION_WINDOW_HOURS",
599
+ );
600
+
601
+ expect(D.datastoreGrowthForecast).toContain(`last ${hours} hours`);
602
+ expect(DATASTORE_DETAIL).toContain("daysToFull <= 365");
603
+ expect(D.datastoreGrowthForecast).toContain("within a year");
604
+ expect(DATASTORE_DETAIL).toContain('"Full now"');
605
+ expect(D.datastoreGrowthForecast).toContain("Full now");
606
+ expect(D.datastoreGrowthForecast).toContain("straight-line");
607
+ });
608
+ });
609
+
610
+ describe("VMware metric descriptions: what each number is made of", () => {
611
+ test("Host CPU is capacity-weighted, and the text says bigger hosts count for more", () => {
612
+ expect(OVERVIEW).toContain("weightedSum += utilization * capacity;");
613
+ expect(D.overviewHostCpu).toContain("bigger hosts counting for more");
614
+ expect(D.overviewHostCpuChart).toContain(
615
+ "Hosts with more CPU count for more",
616
+ );
617
+ });
618
+
619
+ test("the Host Memory chart is a percentage, the Datastore Used chart is summed bytes", () => {
620
+ expect(OVERVIEW).toContain('seriesName: "Host Memory %"');
621
+ expect(D.overviewHostMemoryChart).toContain(
622
+ "as a share of their combined RAM",
623
+ );
624
+ expect(OVERVIEW).toContain('seriesName: "Datastore Used"');
625
+ expect(D.overviewDatastoreUsedChart).toContain("added up");
626
+ expect(D.overviewDatastoreUsedChart).toContain("bytes");
627
+ });
628
+
629
+ test("the VM CPU Ready chart names both of its lines", () => {
630
+ expect(OVERVIEW).toContain('seriesName: "Avg"');
631
+ expect(OVERVIEW).toContain('seriesName: "Max"');
632
+ expect(D.overviewVmCpuReadyChart).toContain("Avg is the average");
633
+ expect(D.overviewVmCpuReadyChart).toContain("Max the highest single VM");
634
+ });
635
+
636
+ test.each([
637
+ "overviewVmCpuReady",
638
+ "overviewVmCpuReadyChart",
639
+ "topVmsByCpuReady",
640
+ "vmCpuReady",
641
+ "vmListCpuReady",
642
+ ] as Array<VMwareMetric>)(
643
+ "%s explains what CPU Ready is",
644
+ (key: VMwareMetric) => {
645
+ expect(D[key]).toContain("physical CPU");
646
+ },
647
+ );
648
+
649
+ test.each([
650
+ "overviewVirtualMachines",
651
+ "vmPowerState",
652
+ "vmListPowerState",
653
+ ] as Array<VMwareMetric>)(
654
+ "%s says power state is inferred and a suspended VM reads as off",
655
+ (key: VMwareMetric) => {
656
+ expect(D[key]).toContain("CPU data");
657
+ expect(D[key]).toContain("suspended");
658
+ },
659
+ );
660
+
661
+ test("the power-state inference the texts describe is the one the code does", () => {
662
+ expect(UTILS).toContain('return "Powered off";');
663
+ expect(OVERVIEW).toContain("if (row.isTemplate) {");
664
+ expect(D.overviewVirtualMachines).toContain("templates excluded");
665
+ });
666
+
667
+ test("the VM Age column is empty because the page never fills it", () => {
668
+ expect(UTILS).toContain('age: "",');
669
+ expect(D.vmListAge).toContain("empty for every virtual machine");
670
+ });
671
+
672
+ test.each([
673
+ "overviewHostEffectiveness",
674
+ "clusterHosts",
675
+ "clusterCpu",
676
+ "clusterMemory",
677
+ "clusterListHosts",
678
+ "clusterListEffectiveCpu",
679
+ "clusterListEffectiveMemory",
680
+ "overviewClusterCount",
681
+ ] as Array<VMwareMetric>)(
682
+ "%s explains what makes a host not effective",
683
+ (key: VMwareMetric) => {
684
+ expect(D[key]).toContain("maintenance mode");
685
+ expect(D[key]).toContain("not responding");
686
+ },
687
+ );
688
+
689
+ test("Host Effectiveness leaves standalone hosts out, as the code does", () => {
690
+ // Only Cluster rows feed clusterHostsTotal.
691
+ expect(OVERVIEW).toContain("clusterHostsTotal += total;");
692
+ expect(D.overviewHostEffectiveness).toContain(
693
+ "standalone hosts are not counted",
694
+ );
695
+ });
696
+
697
+ test("vCenter Health lists what makes it Unhealthy, Degraded and Unknown", () => {
698
+ for (const phrase of [
699
+ "Unhealthy",
700
+ "Degraded",
701
+ "Unknown",
702
+ "maintenance mode",
703
+ "swapped",
704
+ "ballooned",
705
+ "CPU Ready",
706
+ "powered off",
707
+ "disconnected",
708
+ ]) {
709
+ expect({
710
+ phrase,
711
+ found: D.overviewVCenterHealth.includes(phrase),
712
+ }).toEqual({ phrase, found: true });
713
+ }
714
+ expect(OVERVIEW).toContain('["connected", "active"].includes(');
715
+ });
716
+
717
+ test.each([
718
+ "vmMemoryBallooned",
719
+ "resourcePoolListBalloonedSwapped",
720
+ ] as Array<VMwareMetric>)(
721
+ "%s explains ballooning in plain words",
722
+ (key: VMwareMetric) => {
723
+ expect(D[key]).toContain("balloon driver");
724
+ expect(D[key]).toContain("short of RAM");
725
+ },
726
+ );
727
+
728
+ test.each([
729
+ "hostCpu",
730
+ "hostCpuCapacity",
731
+ "hostListCpuCapacity",
732
+ "vmCpu",
733
+ "clusterCpu",
734
+ "clusterListEffectiveCpu",
735
+ "resourcePoolListCpu",
736
+ ] as Array<VMwareMetric>)(
737
+ "%s names the MHz / GHz unit the value is shown in",
738
+ (key: VMwareMetric) => {
739
+ expect(D[key]).toContain("MHz or GHz");
740
+ },
741
+ );
742
+
743
+ test("the unit switch the texts mention is the one formatMhz makes", () => {
744
+ expect(UTILS).toContain("if (Math.abs(value) >= 1000) {");
745
+ expect(UTILS).toContain("GHz");
746
+ expect(UTILS).toContain("MHz");
747
+ });
748
+
749
+ test("the datastore summary card sums every datastore", () => {
750
+ expect(OVERVIEW).toContain(
751
+ "datastoreUsedBytes = (datastoreUsedBytes ?? 0) + used;",
752
+ );
753
+ expect(D.overviewDatastoreCount).toContain("added up across all of them");
754
+ });
755
+
756
+ test("the Host Memory top-5 bar falls back to usage against the top host's usage", () => {
757
+ // maxMemory is the largest USED amount among hosts, not a host's size.
758
+ expect(OVERVIEW).toContain("return Math.max(max, h.memoryBytes ?? 0);");
759
+ expect(OVERVIEW).toContain("((h.memoryBytes ?? 0) / maxMemory) * 100");
760
+ expect(D.topHostsByMemory).toContain(
761
+ "its usage compared with the top host's",
762
+ );
763
+ expect(D.topHostsByMemory).not.toContain("its size");
764
+ });
765
+
766
+ test("the hero count chips are the kinds the inventory-counts text lists", () => {
767
+ const hero: string = OVERVIEW.slice(
768
+ OVERVIEW.indexOf("const specChips:"),
769
+ OVERVIEW.indexOf("const hasCountChips"),
770
+ );
771
+
772
+ for (const [chip, words] of [
773
+ ["datacenter${", "datacenters"],
774
+ ["cluster${", "clusters"],
775
+ ["ESXi host${", "ESXi hosts"],
776
+ ["powered on`", "VMs (powered on out of all, templates excluded)"],
777
+ ["datastore${", "datastores"],
778
+ ] as Array<[string, string]>) {
779
+ expect({ chip, inHero: hero.includes(chip) }).toEqual({
780
+ chip,
781
+ inHero: true,
782
+ });
783
+ expect({
784
+ words,
785
+ inText: D.overviewInventoryCounts.includes(words),
786
+ }).toEqual({ words, inText: true });
787
+ }
788
+ // A zero count hides its chip, which the text says.
789
+ expect(hero).toContain("if (datastoreCount > 0) {");
790
+ expect(D.overviewInventoryCounts).toContain("A kind with none is left out");
791
+ // The agent version chip comes after the cut, so it is not a count.
792
+ expect(hero).not.toContain("agentVersion");
793
+ });
794
+
795
+ test("the fullest-datastore tile is a maximum, not an average", () => {
796
+ expect(OVERVIEW).toContain(
797
+ "if (worstDatastorePercent === null || pct > worstDatastorePercent) {",
798
+ );
799
+ expect(D.overviewDatastores).toContain("fullest datastore");
800
+ });
801
+ });