@oneuptime/common 12.0.34 → 13.0.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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -312,7 +312,7 @@ describe("CompareCriteria aggregation semantics", () => {
312
312
  });
313
313
 
314
314
  expect(message).toBe(
315
- "Any value of CPU is 95, 96, 97 % which is greater than 90 %.",
315
+ "Any value of CPU is 95.00%, 96.00%, 97.00% which is greater than 90.00%.",
316
316
  );
317
317
  });
318
318
 
@@ -328,7 +328,7 @@ describe("CompareCriteria aggregation semantics", () => {
328
328
  unit: "%",
329
329
  });
330
330
 
331
- expect(message).toContain("80, 70");
331
+ expect(message).toContain("80.00%, 70.00%");
332
332
  expect(message).not.toContain("95");
333
333
  });
334
334
 
@@ -345,7 +345,7 @@ describe("CompareCriteria aggregation semantics", () => {
345
345
  });
346
346
 
347
347
  expect(message).toBe(
348
- "All values of CPU is 95, 96 % which is greater than 90 %.",
348
+ "All values of CPU is 95.00%, 96.00% which is greater than 90.00%.",
349
349
  );
350
350
  });
351
351
 
@@ -513,15 +513,21 @@ describe("CompareCriteria aggregation semantics", () => {
513
513
  /*
514
514
  * The guard against over-suppression. If this ever goes quiet the
515
515
  * fix has widened into a general unit filter.
516
+ *
517
+ * The unit now rides each NUMBER at the scale that number landed on,
518
+ * so a sub-byte value reports "B" and 0.85 ms is written out as the
519
+ * 850 µs it is. Both halves of the sentence go through the same
520
+ * renderer, which is the property that matters here: the observed
521
+ * value and the threshold can never be quoted in different units.
516
522
  */
517
523
  expect(unitMessage("By")).toBe(
518
- "Any value of Metric Value is 0.85 By which is greater than 0.8 By.",
524
+ "Any value of Metric Value is 0.85 B which is greater than 0.8 B.",
519
525
  );
520
526
  expect(unitMessage("%")).toBe(
521
- "Any value of Metric Value is 0.85 % which is greater than 0.8 %.",
527
+ "Any value of Metric Value is 0.85% which is greater than 0.80%.",
522
528
  );
523
529
  expect(unitMessage("ms")).toBe(
524
- "Any value of Metric Value is 0.85 ms which is greater than 0.8 ms.",
530
+ "Any value of Metric Value is 850 µs which is greater than 800 µs.",
525
531
  );
526
532
  // "1" is suppressed; a unit that merely CONTAINS a 1 is not.
527
533
  expect(unitMessage("m/s2")).toContain("0.85 m/s2");
@@ -532,7 +538,7 @@ describe("CompareCriteria aggregation semantics", () => {
532
538
  const message: string = unitMessage(" ms ");
533
539
 
534
540
  expect(message).toBe(
535
- "Any value of Metric Value is 0.85 ms which is greater than 0.8 ms.",
541
+ "Any value of Metric Value is 850 µs which is greater than 800 µs.",
536
542
  );
537
543
  expect(message).not.toContain(" ");
538
544
  });
@@ -178,6 +178,35 @@ describe("DatabaseMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
178
178
  expect(result).toContain("95");
179
179
  });
180
180
 
181
+ /*
182
+ * A Database Health metric carries an explicit catalog unit, and
183
+ * DatabaseMonitorCriteria hands that unit to CompareCriteria for the
184
+ * alert email. The observation beside it used to render bytes with a
185
+ * 1024 divisor while labelling the rungs SI, so one byte count read
186
+ * three ways across the product: "1.07 GB" in the email, "1.00 GB" in
187
+ * the evaluation log, and "1000000000 bytes" in the expectation
188
+ * clause. All three now agree.
189
+ */
190
+ test("a bytes metric is rescaled decimally, matching the alert email", async () => {
191
+ const result: string | null = await evaluate(
192
+ buildDataToProcess({
193
+ metrics: {
194
+ [MonitorMetricType.DatabaseSizeBytes]: 1073741824,
195
+ },
196
+ }),
197
+ metricFilter(
198
+ MonitorMetricType.DatabaseSizeBytes,
199
+ FilterType.GreaterThan,
200
+ "1000000000",
201
+ ),
202
+ );
203
+
204
+ expect(result).toContain("1.07 GB");
205
+ // The 1024-based rendering this used to produce.
206
+ expect(result).not.toContain("1.00 GB");
207
+ expect(result).not.toContain("1073741824");
208
+ });
209
+
181
210
  test("percent metric 40 > 90 -> not met", async () => {
182
211
  const result: string | null = await evaluate(
183
212
  buildDataToProcess({
@@ -111,8 +111,15 @@ describe("MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
111
111
  await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet(inputs);
112
112
 
113
113
  expect(message).toBeTruthy();
114
- expect(message).toContain("greater than 2000 ms");
115
- expect(message).toContain("2500 ms");
114
+ /*
115
+ * The COMPARISON happens in the metric's native ms — that is what this
116
+ * test is about, and the context below pins it. The MESSAGE renders
117
+ * those same milliseconds at the scale a human reads them at, so 2500
118
+ * ms is written "2.5 sec"; the threshold gets the same treatment so
119
+ * the two halves of the sentence cannot disagree.
120
+ */
121
+ expect(message).toContain("greater than 2 sec");
122
+ expect(message).toContain("2.5 sec");
116
123
  expect(criteriaFilter.metricCriteriaContext?.unit).toBe("ms");
117
124
  expect(criteriaFilter.metricCriteriaContext?.breachingSample?.value).toBe(
118
125
  2500,
@@ -228,11 +235,23 @@ describe("MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
228
235
  await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet(inputs);
229
236
 
230
237
  /*
231
- * No conversion possible, so the displayed unit/values use user's MB
232
- * label but values are passed through.
238
+ * No conversion possible, so the sample value is passed through
239
+ * untouched and the criteria still speaks the user's chosen MB. Those
240
+ * two facts are the subject of this test and are asserted directly on
241
+ * the context.
242
+ *
243
+ * The rendered sentence is deliberately NOT asserted on: the user has
244
+ * mislabelled a 5000 ms sample as 5000 MB, and the renderer takes that
245
+ * label at its word and reads it out as "5 GB". Any string this
246
+ * sentence can produce is wrong, because the input is wrong — pinning
247
+ * one spelling of a wrong answer would only make the misconfiguration
248
+ * look supported.
233
249
  */
234
250
  expect(message).toBeTruthy();
235
- expect(message).toContain("MB");
251
+ expect(criteriaFilter.metricCriteriaContext?.unit).toBe("MB");
252
+ expect(criteriaFilter.metricCriteriaContext?.breachingSample?.value).toBe(
253
+ 5000,
254
+ );
236
255
  });
237
256
 
238
257
  test("no data + NoDataPolicy.Trigger triggers regardless of threshold", async () => {
@@ -495,7 +514,12 @@ describe("MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
495
514
  await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet(inputs);
496
515
 
497
516
  expect(message).toBeTruthy();
498
- expect(message).toContain("10 samples between 110 and 200");
517
+ /*
518
+ * Both ends of the summary carry the unit, because either end can
519
+ * land on a different scale than the other once values spread far
520
+ * enough apart.
521
+ */
522
+ expect(message).toContain("10 samples between 110 ms and 200 ms");
499
523
  expect(message).toContain("greater than 100 ms");
500
524
  // The raw comma-joined dump is no longer in the message
501
525
  expect(message).not.toContain("110, 120, 130, 140, 150, 160");
@@ -0,0 +1,246 @@
1
+ /*
2
+ * The baseline lookup is the only thing standing between this test and
3
+ * ClickHouse. Stub it so the anomaly branch runs against a known baseline
4
+ * and the assertions are about the SENTENCE, which is what an alert email
5
+ * shows.
6
+ */
7
+ jest.mock("../../../../../Server/Services/MetricBaselineService", () => {
8
+ const actual: Record<string, unknown> = jest.requireActual(
9
+ "../../../../../Server/Services/MetricBaselineService",
10
+ ) as Record<string, unknown>;
11
+
12
+ return {
13
+ ...actual,
14
+ __esModule: true,
15
+ default: {
16
+ getBaseline: jest.fn(),
17
+ },
18
+ };
19
+ });
20
+
21
+ import MetricMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/MetricMonitorCriteria";
22
+ import MetricBaselineService, {
23
+ BaselineSummary,
24
+ } from "../../../../../Server/Services/MetricBaselineService";
25
+ import AggregateModel from "../../../../../Types/BaseDatabase/AggregatedModel";
26
+ import AggregatedResult from "../../../../../Types/BaseDatabase/AggregatedResult";
27
+ import {
28
+ CheckOn,
29
+ CriteriaFilter,
30
+ FilterType,
31
+ } from "../../../../../Types/Monitor/CriteriaFilter";
32
+ import MetricAliasData from "../../../../../Types/Metrics/MetricAliasData";
33
+ import MetricQueryConfigData from "../../../../../Types/Metrics/MetricQueryConfigData";
34
+ import MetricQueryData from "../../../../../Types/Metrics/MetricQueryData";
35
+ import MetricsViewConfig from "../../../../../Types/Metrics/MetricsViewConfig";
36
+ import MetricMonitorResponse from "../../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
37
+ import MonitorStep from "../../../../../Types/Monitor/MonitorStep";
38
+ import RollingTime from "../../../../../Types/RollingTime/RollingTime";
39
+ import ObjectID from "../../../../../Types/ObjectID";
40
+ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
41
+
42
+ /*
43
+ * THE ANOMALY SENTENCE.
44
+ *
45
+ * An anomaly breach produces its own root cause — it never reaches
46
+ * CompareCriteria — and it used to print `value.toFixed(3)` with the raw
47
+ * exporter unit glued on:
48
+ *
49
+ * "container.memory.usage value 1073741824.000 By is 4.20σ above ..."
50
+ *
51
+ * The quantities in it are now rendered exactly as the Breaching Samples
52
+ * table renders them. σ is NOT one of those quantities: it is a count of
53
+ * standard deviations, dimensionless by construction, and must stay a
54
+ * bare number no matter what unit the metric carries.
55
+ */
56
+
57
+ const BASELINE_MEAN: number = 268435456;
58
+ const BASELINE_STDDEV: number = 134217728;
59
+ const BREACHING_VALUE: number = 1073741824;
60
+
61
+ function baseline(): BaselineSummary {
62
+ return {
63
+ sampleCount: 480,
64
+ mean: BASELINE_MEAN,
65
+ stddev: BASELINE_STDDEV,
66
+ median: BASELINE_MEAN,
67
+ p95: BASELINE_MEAN,
68
+ minObserved: 0,
69
+ maxObserved: BREACHING_VALUE,
70
+ isReliable: true,
71
+ windowDays: 14,
72
+ hourOfWeek: 0,
73
+ } as BaselineSummary;
74
+ }
75
+
76
+ function buildInputs(input: {
77
+ metricNativeUnit: string;
78
+ metricName: string;
79
+ sampleValue: number;
80
+ }): {
81
+ criteriaFilter: CriteriaFilter;
82
+ monitorStep: MonitorStep;
83
+ dataToProcess: MetricMonitorResponse;
84
+ projectId: ObjectID;
85
+ } {
86
+ const aliasData: MetricAliasData = {
87
+ metricVariable: "a",
88
+ title: input.metricName,
89
+ description: undefined,
90
+ legend: undefined,
91
+ legendUnit: input.metricNativeUnit,
92
+ };
93
+
94
+ const queryConfig: MetricQueryConfigData = {
95
+ metricAliasData: aliasData,
96
+ metricQueryData: {
97
+ filterData: {
98
+ metricName: input.metricName,
99
+ },
100
+ } as unknown as MetricQueryData,
101
+ };
102
+
103
+ const metricViewConfig: MetricsViewConfig = {
104
+ queryConfigs: [queryConfig],
105
+ formulaConfigs: [],
106
+ };
107
+
108
+ const monitorStep: MonitorStep = new MonitorStep();
109
+ monitorStep.data = {
110
+ id: ObjectID.generate().toString(),
111
+ monitorCriteria: { data: undefined } as never,
112
+ } as unknown as MonitorStep["data"];
113
+ monitorStep.data!.metricMonitor = {
114
+ metricViewConfig,
115
+ rollingTime: RollingTime.Past1Minute,
116
+ };
117
+
118
+ const aggregated: AggregatedResult = {
119
+ data: [
120
+ {
121
+ timestamp: new Date("2026-08-14T10:30:00.000Z"),
122
+ value: input.sampleValue,
123
+ } as AggregateModel,
124
+ ],
125
+ };
126
+
127
+ const projectId: ObjectID = ObjectID.generate();
128
+
129
+ return {
130
+ criteriaFilter: {
131
+ checkOn: CheckOn.MetricValue,
132
+ filterType: FilterType.AnomalouslyHigh,
133
+ metricMonitorOptions: {
134
+ metricAlias: "a",
135
+ },
136
+ } as CriteriaFilter,
137
+ monitorStep,
138
+ dataToProcess: {
139
+ projectId: projectId,
140
+ metricResult: [aggregated],
141
+ metricViewConfig,
142
+ monitorId: ObjectID.generate(),
143
+ },
144
+ projectId,
145
+ };
146
+ }
147
+
148
+ async function anomalyRootCause(input: {
149
+ metricNativeUnit: string;
150
+ metricName: string;
151
+ sampleValue: number;
152
+ }): Promise<string> {
153
+ const inputs: ReturnType<typeof buildInputs> = buildInputs(input);
154
+
155
+ const message: string | null =
156
+ await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
157
+ criteriaFilter: inputs.criteriaFilter,
158
+ monitorStep: inputs.monitorStep,
159
+ dataToProcess: inputs.dataToProcess,
160
+ projectId: inputs.projectId,
161
+ } as never);
162
+
163
+ expect(message).toBeTruthy();
164
+ return message as string;
165
+ }
166
+
167
+ describe("MetricMonitorCriteria - anomaly root cause units", () => {
168
+ beforeEach(() => {
169
+ (MetricBaselineService.getBaseline as unknown as jest.Mock).mockReset();
170
+ (
171
+ MetricBaselineService.getBaseline as unknown as jest.Mock
172
+ ).mockResolvedValue(baseline() as never);
173
+ });
174
+
175
+ test("value, mean and stddev are all rendered at human scale", async () => {
176
+ const rootCause: string = await anomalyRootCause({
177
+ metricName: "container.memory.usage",
178
+ metricNativeUnit: "By",
179
+ sampleValue: BREACHING_VALUE,
180
+ });
181
+
182
+ expect(rootCause).toContain("value 1.07 GB");
183
+ expect(rootCause).toContain("mean 268 MB");
184
+ expect(rootCause).toContain("σ 134 MB");
185
+
186
+ // The old rendering, in full.
187
+ expect(rootCause).not.toContain("1073741824.000");
188
+ expect(rootCause).not.toContain("268435456.000");
189
+ expect(rootCause).not.toContain("By");
190
+ });
191
+
192
+ /*
193
+ * The σ COUNT is not a quantity in the metric's unit — "6.00σ above the
194
+ * baseline" is six standard deviations, and rendering it as "6 GB"
195
+ * would be nonsense. It keeps its own two-decimal format.
196
+ */
197
+ test("the sigma count stays a bare number", async () => {
198
+ const rootCause: string = await anomalyRootCause({
199
+ metricName: "container.memory.usage",
200
+ metricNativeUnit: "By",
201
+ sampleValue: BREACHING_VALUE,
202
+ });
203
+
204
+ expect(rootCause).toMatch(/is \d+\.\d{2}σ above the same-hour baseline/);
205
+ expect(rootCause).not.toMatch(/is [\d.]+ [A-Za-z]+σ/);
206
+ });
207
+
208
+ test("a metric with no unit keeps its exact digits", async () => {
209
+ (
210
+ MetricBaselineService.getBaseline as unknown as jest.Mock
211
+ ).mockResolvedValue({
212
+ ...baseline(),
213
+ mean: 100,
214
+ stddev: 20,
215
+ } as never);
216
+
217
+ const rootCause: string = await anomalyRootCause({
218
+ metricName: "http.server.request.count",
219
+ metricNativeUnit: "",
220
+ sampleValue: 5000,
221
+ });
222
+
223
+ expect(rootCause).toContain("value 5000 ");
224
+ expect(rootCause).not.toContain("5K");
225
+ });
226
+
227
+ test("a ratio metric reads as a percentage", async () => {
228
+ (
229
+ MetricBaselineService.getBaseline as unknown as jest.Mock
230
+ ).mockResolvedValue({
231
+ ...baseline(),
232
+ mean: 0.2,
233
+ stddev: 0.05,
234
+ } as never);
235
+
236
+ const rootCause: string = await anomalyRootCause({
237
+ metricName: "system.cpu.utilization",
238
+ metricNativeUnit: "1",
239
+ sampleValue: 0.85,
240
+ });
241
+
242
+ expect(rootCause).toContain("value 85.00%");
243
+ expect(rootCause).toContain("mean 20.00%");
244
+ expect(rootCause).not.toContain("0.85 1");
245
+ });
246
+ });
@@ -0,0 +1,280 @@
1
+ import MonitorCriteriaMessageBuilder from "../../../../Server/Utils/Monitor/MonitorCriteriaMessageBuilder";
2
+ import CompareCriteria from "../../../../Server/Utils/Monitor/Criteria/CompareCriteria";
3
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
4
+ import {
5
+ CheckOn,
6
+ CriteriaFilter,
7
+ EvaluateOverTimeType,
8
+ FilterType,
9
+ } from "../../../../Types/Monitor/CriteriaFilter";
10
+ import AggregateModel from "../../../../Types/BaseDatabase/AggregatedModel";
11
+ import AggregatedResult from "../../../../Types/BaseDatabase/AggregatedResult";
12
+ import MetricAliasData from "../../../../Types/Metrics/MetricAliasData";
13
+ import MetricQueryConfigData from "../../../../Types/Metrics/MetricQueryConfigData";
14
+ import MetricQueryData from "../../../../Types/Metrics/MetricQueryData";
15
+ import MetricsViewConfig from "../../../../Types/Metrics/MetricsViewConfig";
16
+ import MetricMonitorResponse from "../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
17
+ import MonitorStep from "../../../../Types/Monitor/MonitorStep";
18
+ import RollingTime from "../../../../Types/RollingTime/RollingTime";
19
+ import ObjectID from "../../../../Types/ObjectID";
20
+ import { describe, expect, test } from "@jest/globals";
21
+
22
+ /*
23
+ * ONE SAMPLE, ONE DESCRIPTION.
24
+ *
25
+ * A metric breach is written twice, by two different code paths, and a
26
+ * user compares them side by side:
27
+ *
28
+ * - the ALERT EMAIL's "Filter Conditions Met" line, produced by
29
+ * CompareCriteria when the filter MATCHED, and
30
+ * - the monitor's EVALUATION LOG line, produced by the observation and
31
+ * expectation builders when the filter did NOT match.
32
+ *
33
+ * Only the first was migrated to MetricValueFormatter, so the same
34
+ * 1073741824-byte sample read "1.07 GB" in the inbox and
35
+ * "1073741824.00 By" on the monitor page — and a ratio metric read "6.00%"
36
+ * in one and "0.06" in the other.
37
+ *
38
+ * These tests assert the two paths render the same quantity the same way.
39
+ * They are deliberately about AGREEMENT rather than about either exact
40
+ * wording: the sentences around the number differ by design, the number
41
+ * and its unit must not.
42
+ */
43
+
44
+ function buildInputs(input: {
45
+ metricNativeUnit: string;
46
+ metricName: string;
47
+ sampleValues: Array<number>;
48
+ criteriaFilter: CriteriaFilter;
49
+ }): {
50
+ monitor: Monitor;
51
+ criteriaFilter: CriteriaFilter;
52
+ monitorStep: MonitorStep;
53
+ dataToProcess: MetricMonitorResponse;
54
+ } {
55
+ const aliasData: MetricAliasData = {
56
+ metricVariable: "a",
57
+ title: input.metricName,
58
+ description: undefined,
59
+ legend: undefined,
60
+ legendUnit: input.metricNativeUnit,
61
+ };
62
+
63
+ const queryConfig: MetricQueryConfigData = {
64
+ metricAliasData: aliasData,
65
+ metricQueryData: {
66
+ filterData: {
67
+ metricName: input.metricName,
68
+ },
69
+ } as unknown as MetricQueryData,
70
+ };
71
+
72
+ const metricViewConfig: MetricsViewConfig = {
73
+ queryConfigs: [queryConfig],
74
+ formulaConfigs: [],
75
+ };
76
+
77
+ const monitorStep: MonitorStep = new MonitorStep();
78
+ monitorStep.data = {
79
+ id: ObjectID.generate().toString(),
80
+ monitorCriteria: { data: undefined } as never,
81
+ } as unknown as MonitorStep["data"];
82
+ monitorStep.data!.metricMonitor = {
83
+ metricViewConfig,
84
+ rollingTime: RollingTime.Past1Minute,
85
+ };
86
+
87
+ const aggregated: AggregatedResult = {
88
+ data: input.sampleValues.map((v: number) => {
89
+ return {
90
+ timestamp: new Date(),
91
+ value: v,
92
+ } as AggregateModel;
93
+ }),
94
+ };
95
+
96
+ return {
97
+ monitor: new Monitor(),
98
+ criteriaFilter: input.criteriaFilter,
99
+ monitorStep,
100
+ dataToProcess: {
101
+ projectId: ObjectID.generate(),
102
+ metricResult: [aggregated],
103
+ metricViewConfig,
104
+ monitorId: ObjectID.generate(),
105
+ },
106
+ };
107
+ }
108
+
109
+ interface AgreementCase {
110
+ name: string;
111
+ metricName: string;
112
+ nativeUnit: string;
113
+ sample: number;
114
+ threshold: string;
115
+ /** The rendering both paths must produce for `sample`. */
116
+ expectedValue: string;
117
+ /** The rendering both paths must produce for `threshold`. */
118
+ expectedThreshold: string;
119
+ }
120
+
121
+ const CASES: Array<AgreementCase> = [
122
+ {
123
+ name: "bytes rescale to gigabytes",
124
+ metricName: "k8s.pod.memory.usage",
125
+ nativeUnit: "By",
126
+ sample: 1073741824,
127
+ threshold: "1000000000",
128
+ expectedValue: "1.07 GB",
129
+ expectedThreshold: "1 GB",
130
+ },
131
+ {
132
+ name: "milliseconds rescale to seconds",
133
+ metricName: "http.server.request.duration",
134
+ nativeUnit: "ms",
135
+ sample: 1500,
136
+ threshold: "1000",
137
+ expectedValue: "1.5 sec",
138
+ expectedThreshold: "1 sec",
139
+ },
140
+ {
141
+ name: "a fraction metric reads as a percentage",
142
+ metricName: "system.cpu.utilization",
143
+ nativeUnit: "1",
144
+ sample: 0.0585,
145
+ threshold: "0.05",
146
+ expectedValue: "5.85%",
147
+ expectedThreshold: "5.00%",
148
+ },
149
+ {
150
+ name: "a non-fraction metric carrying '1' stays bare",
151
+ metricName: "browser.cumulative_layout_shift",
152
+ nativeUnit: "1",
153
+ sample: 0.31,
154
+ threshold: "0.25",
155
+ expectedValue: "0.31",
156
+ expectedThreshold: "0.25",
157
+ },
158
+ {
159
+ name: "a metric with no unit keeps its exact digits",
160
+ metricName: "http.server.request.count",
161
+ nativeUnit: "",
162
+ sample: 5000,
163
+ threshold: "4900",
164
+ expectedValue: "5000",
165
+ expectedThreshold: "4900",
166
+ },
167
+ ];
168
+
169
+ describe("the alert email and the evaluation log describe one sample identically", () => {
170
+ for (const testCase of CASES) {
171
+ test(testCase.name, () => {
172
+ const criteriaFilter: CriteriaFilter = {
173
+ checkOn: CheckOn.MetricValue,
174
+ filterType: FilterType.GreaterThan,
175
+ value: testCase.threshold,
176
+ metricMonitorOptions: {
177
+ metricAlias: "a",
178
+ metricAggregationType: EvaluateOverTimeType.AnyValue,
179
+ },
180
+ };
181
+
182
+ const inputs: ReturnType<typeof buildInputs> = buildInputs({
183
+ metricNativeUnit: testCase.nativeUnit,
184
+ metricName: testCase.metricName,
185
+ sampleValues: [testCase.sample],
186
+ criteriaFilter,
187
+ });
188
+
189
+ /*
190
+ * What the alert email says — CompareCriteria's sentence, which is
191
+ * returned verbatim as the root cause when the filter matched.
192
+ */
193
+ const emailLine: string = CompareCriteria.getCompareMessage({
194
+ values: testCase.sample,
195
+ threshold: Number(testCase.threshold),
196
+ criteriaFilter: criteriaFilter,
197
+ metricDisplayName: testCase.metricName,
198
+ metricName: testCase.metricName,
199
+ unit: testCase.nativeUnit || undefined,
200
+ });
201
+
202
+ // What the monitor's evaluation log says for the same sample.
203
+ const evalLogLine: string =
204
+ MonitorCriteriaMessageBuilder.buildCriteriaFilterMessage({
205
+ ...inputs,
206
+ didMeetCriteria: false,
207
+ matchMessage: null,
208
+ });
209
+
210
+ expect(emailLine).toContain(testCase.expectedValue);
211
+ expect(evalLogLine).toContain(testCase.expectedValue);
212
+
213
+ expect(emailLine).toContain(testCase.expectedThreshold);
214
+ expect(evalLogLine).toContain(testCase.expectedThreshold);
215
+ });
216
+ }
217
+
218
+ /*
219
+ * The specific strings the drift used to produce. Asserting their
220
+ * absence is what makes this suite fail loudly if either path is
221
+ * reverted to its own formatter.
222
+ */
223
+ test("neither path emits the pre-migration renderings", () => {
224
+ const criteriaFilter: CriteriaFilter = {
225
+ checkOn: CheckOn.MetricValue,
226
+ filterType: FilterType.GreaterThan,
227
+ value: "1000000000",
228
+ metricMonitorOptions: {
229
+ metricAlias: "a",
230
+ metricAggregationType: EvaluateOverTimeType.AnyValue,
231
+ },
232
+ };
233
+
234
+ const inputs: ReturnType<typeof buildInputs> = buildInputs({
235
+ metricNativeUnit: "By",
236
+ metricName: "k8s.pod.memory.usage",
237
+ sampleValues: [1073741824],
238
+ criteriaFilter,
239
+ });
240
+
241
+ const evalLogLine: string =
242
+ MonitorCriteriaMessageBuilder.buildCriteriaFilterMessage({
243
+ ...inputs,
244
+ didMeetCriteria: false,
245
+ matchMessage: null,
246
+ });
247
+
248
+ expect(evalLogLine).not.toContain("1073741824");
249
+ expect(evalLogLine).not.toContain(" By");
250
+ });
251
+
252
+ /*
253
+ * A matched filter short-circuits: the message builder returns
254
+ * CompareCriteria's sentence untouched. Pinning this keeps the two
255
+ * paths from being confused for one another — the agreement above is a
256
+ * property of two DIFFERENT renderers, not of one shared call.
257
+ */
258
+ test("a matched filter returns the email sentence verbatim", () => {
259
+ const criteriaFilter: CriteriaFilter = {
260
+ checkOn: CheckOn.MetricValue,
261
+ filterType: FilterType.GreaterThan,
262
+ value: "1000000000",
263
+ };
264
+
265
+ const inputs: ReturnType<typeof buildInputs> = buildInputs({
266
+ metricNativeUnit: "By",
267
+ metricName: "k8s.pod.memory.usage",
268
+ sampleValues: [1073741824],
269
+ criteriaFilter,
270
+ });
271
+
272
+ expect(
273
+ MonitorCriteriaMessageBuilder.buildCriteriaFilterMessage({
274
+ ...inputs,
275
+ didMeetCriteria: true,
276
+ matchMessage: "Metric Value is 1.07 GB which is greater than 1 GB.",
277
+ }),
278
+ ).toBe("Metric Value is 1.07 GB which is greater than 1 GB.");
279
+ });
280
+ });