@oneuptime/common 14.0.0 → 14.0.1

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 (196) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
  3. package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
  4. package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
  5. package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  9. package/Server/Services/DatabaseService.ts +10 -0
  10. package/Server/Services/Index.ts +4 -0
  11. package/Server/Services/MonitorService.ts +3 -2
  12. package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
  13. package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
  14. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
  15. package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
  16. package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
  17. package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
  18. package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
  19. package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
  20. package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
  21. package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
  25. package/Server/Utils/Monitor/RootCauseList.ts +139 -0
  26. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
  27. package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
  28. package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
  29. package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
  30. package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
  31. package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
  32. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
  33. package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
  34. package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
  35. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
  36. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
  37. package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
  38. package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
  39. package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
  40. package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
  41. package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
  42. package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
  43. package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
  44. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
  45. package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
  46. package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
  47. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  48. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  49. package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
  50. package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
  51. package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
  52. package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
  53. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
  54. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
  55. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
  56. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
  57. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
  58. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
  59. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
  60. package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
  61. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
  62. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
  63. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
  64. package/Tests/Server/Types/Markdown.test.ts +2 -1
  65. package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
  66. package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
  67. package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
  68. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
  69. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
  71. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
  72. package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
  73. package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
  74. package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
  75. package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
  76. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
  77. package/Tests/Types/API/Route.test.ts +56 -0
  78. package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
  79. package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
  80. package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
  81. package/Tests/UI/Components/Icon.test.tsx +26 -0
  82. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  83. package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
  84. package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
  85. package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
  86. package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
  87. package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
  88. package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
  89. package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
  90. package/Types/API/Route.ts +19 -2
  91. package/Types/API/URL.ts +26 -2
  92. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
  93. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
  94. package/Types/Permission.ts +90 -0
  95. package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
  96. package/Types/Rules/RuleRun.ts +4 -0
  97. package/UI/Components/Feed/FeedOptions.ts +427 -0
  98. package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
  99. package/UI/Components/Feed/useFeedItems.ts +28 -9
  100. package/UI/Components/Feed/useFeedOptions.ts +225 -0
  101. package/UI/Components/Icon/Icon.tsx +1 -1
  102. package/Utils/LabelRuleImportExport.ts +27 -9
  103. package/Utils/Monitor/MetricValueFormatter.ts +2 -2
  104. package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
  105. package/Utils/Slo/SloDuration.ts +22 -6
  106. package/Utils/Slo/SloHealth.ts +8 -7
  107. package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
  108. package/Utils/Slo/SloOverviewText.ts +33 -0
  109. package/Utils/Slo/SloProjection.ts +4 -3
  110. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  111. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
  113. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
  115. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
  116. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
  117. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
  119. package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +7 -0
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/Index.js +4 -0
  129. package/build/dist/Server/Services/Index.js.map +1 -1
  130. package/build/dist/Server/Services/MonitorService.js +3 -2
  131. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  132. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
  133. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
  134. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
  135. package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
  136. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
  137. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
  138. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
  139. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
  140. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
  141. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
  142. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
  143. package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
  144. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
  145. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  146. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
  147. package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
  148. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
  149. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
  150. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
  151. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
  152. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  154. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
  155. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  156. package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
  157. package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
  158. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
  159. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  160. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
  161. package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
  162. package/build/dist/Types/API/Route.js +18 -2
  163. package/build/dist/Types/API/Route.js.map +1 -1
  164. package/build/dist/Types/API/URL.js +23 -2
  165. package/build/dist/Types/API/URL.js.map +1 -1
  166. package/build/dist/Types/Permission.js +84 -0
  167. package/build/dist/Types/Permission.js.map +1 -1
  168. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
  169. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  170. package/build/dist/Types/Rules/RuleRun.js +4 -0
  171. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  172. package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
  173. package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
  174. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
  175. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
  176. package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
  177. package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
  178. package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
  179. package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
  180. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  181. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  182. package/build/dist/Utils/LabelRuleImportExport.js +22 -6
  183. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  184. package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
  185. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
  186. package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
  187. package/build/dist/Utils/Slo/SloDuration.js +13 -5
  188. package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
  189. package/build/dist/Utils/Slo/SloHealth.js +8 -7
  190. package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
  191. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
  192. package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
  193. package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
  194. package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
  195. package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
  196. package/package.json +1 -1
@@ -23,6 +23,10 @@ import {
23
23
  getVMwareAlertTemplateById,
24
24
  VMwareAlertTemplate,
25
25
  } from "../../../../Types/Monitor/VMwareAlertTemplates";
26
+ import {
27
+ getKubernetesAlertTemplateById,
28
+ KubernetesAlertTemplate,
29
+ } from "../../../../Types/Monitor/KubernetesAlertTemplates";
26
30
  import ObjectID from "../../../../Types/ObjectID";
27
31
  import MonitorCriteriaInstance from "../../../../Types/Monitor/MonitorCriteriaInstance";
28
32
  import {
@@ -78,6 +82,7 @@ type EvaluatorPrivate = {
78
82
  dataToProcess: unknown;
79
83
  monitorStep: MonitorStep;
80
84
  monitor: Monitor;
85
+ criteriaInstance?: MonitorCriteriaInstance | undefined;
81
86
  }) => Promise<string | null>;
82
87
  buildDockerSwarmRootCauseContext: (input: {
83
88
  dataToProcess: unknown;
@@ -1156,6 +1161,406 @@ describe("MonitorCriteriaEvaluator - Ceph affected-resources breach predicate",
1156
1161
  expect(context).toContain("1. **Daemon** `osd.4` — **1**");
1157
1162
  expect(context).not.toContain("osd.3");
1158
1163
  });
1164
+
1165
+ test("an `= 0` criteria that opens an incident still keeps the `> 0` fallback (only Kubernetes opts in)", () => {
1166
+ /*
1167
+ * Kubernetes treats a FIRING `= 0` as a fall (node Ready = 0). Ceph
1168
+ * must not: every Ceph `= 0` criteria is a recovery, and the shared
1169
+ * predicate only inverts for it when the caller asks.
1170
+ */
1171
+ const firingEqualsZero: MonitorCriteriaInstance = syntheticCriteria([
1172
+ {
1173
+ checkOn: CheckOn.MetricValue,
1174
+ filterType: FilterType.EqualTo,
1175
+ value: 0,
1176
+ },
1177
+ ]);
1178
+ firingEqualsZero.data!.createIncidents = true;
1179
+ firingEqualsZero.data!.createAlerts = true;
1180
+
1181
+ const context: string | null = cephContext({
1182
+ criteriaInstance: firingEqualsZero,
1183
+ affectedResources: [
1184
+ { daemon: "osd.3", metricValue: 0 },
1185
+ { daemon: "osd.4", metricValue: 1 },
1186
+ ],
1187
+ });
1188
+
1189
+ expect(context).toContain("1. **Daemon** `osd.4` — **1**");
1190
+ expect(context).not.toContain("osd.3");
1191
+ });
1192
+ });
1193
+
1194
+ /*
1195
+ * REGRESSION: the Kubernetes "Affected Resources" list named the HEALTHY
1196
+ * resources for criteria that fire when the metric FALLS.
1197
+ *
1198
+ * buildKubernetesRootCauseContext filtered the breakdown with a hardcoded
1199
+ * `metricValue > 0` and sorted highest first. k8s-node-not-ready fires on
1200
+ * `k8s.node.condition_ready = 0` and k8s-etcd-no-leader on
1201
+ * `etcd_server_has_leader = 0`, so the zero rows ARE the breach: the
1202
+ * filter dropped the NotReady node, listed the Ready ones, and the Root
1203
+ * Cause Analysis told the reader "Node `node-ok-1` is reporting NotReady
1204
+ * (value: 1)" and to `kubectl describe node node-ok-1`.
1205
+ *
1206
+ * The worker also kept only the HIGHEST sample per resource, so a node
1207
+ * that was NotReady for one scrape and Ready for the rest reached the
1208
+ * evaluator as 1. It now also records the lowest sample, which is what a
1209
+ * fall criteria breached on.
1210
+ */
1211
+ describe("MonitorCriteriaEvaluator - Kubernetes affected-resources breach predicate", () => {
1212
+ function kubernetesStep(templateId: string): MonitorStep {
1213
+ const template: KubernetesAlertTemplate | undefined =
1214
+ getKubernetesAlertTemplateById(templateId);
1215
+ if (!template) {
1216
+ throw new Error(`${templateId} template missing`);
1217
+ }
1218
+ return template.getMonitorStep(templateArgs());
1219
+ }
1220
+
1221
+ function criteriaInstances(
1222
+ monitorStep: MonitorStep,
1223
+ ): Array<MonitorCriteriaInstance> {
1224
+ return monitorStep.data!.monitorCriteria!.data!
1225
+ .monitorCriteriaInstanceArray;
1226
+ }
1227
+
1228
+ /** The template's FIRING criteria (the one that opens the incident). */
1229
+ function firingCriteria(monitorStep: MonitorStep): MonitorCriteriaInstance {
1230
+ const instance: MonitorCriteriaInstance | undefined =
1231
+ criteriaInstances(monitorStep)[0];
1232
+ if (!instance) {
1233
+ throw new Error("firing criteria instance missing");
1234
+ }
1235
+ return instance;
1236
+ }
1237
+
1238
+ /** The template's RECOVERY criteria. */
1239
+ function recoveryCriteria(monitorStep: MonitorStep): MonitorCriteriaInstance {
1240
+ const instances: Array<MonitorCriteriaInstance> =
1241
+ criteriaInstances(monitorStep);
1242
+ const instance: MonitorCriteriaInstance | undefined =
1243
+ instances[instances.length - 1];
1244
+ if (!instance) {
1245
+ throw new Error("recovery criteria instance missing");
1246
+ }
1247
+ return instance;
1248
+ }
1249
+
1250
+ function syntheticCriteria(input: {
1251
+ filters: Array<CriteriaFilter>;
1252
+ opensIncident: boolean;
1253
+ }): MonitorCriteriaInstance {
1254
+ const instance: MonitorCriteriaInstance = new MonitorCriteriaInstance();
1255
+ instance.data = {
1256
+ monitorStatusId: undefined,
1257
+ filterCondition: FilterCondition.Any,
1258
+ filters: input.filters,
1259
+ incidents: [],
1260
+ alerts: [],
1261
+ name: "synthetic",
1262
+ description: "synthetic",
1263
+ createIncidents: input.opensIncident,
1264
+ createAlerts: input.opensIncident,
1265
+ id: ObjectID.generate().toString(),
1266
+ };
1267
+ return instance;
1268
+ }
1269
+
1270
+ async function kubernetesContext(input: {
1271
+ metricName: string;
1272
+ affectedResources: Array<KubernetesAffectedResource>;
1273
+ monitorStep?: MonitorStep | undefined;
1274
+ criteriaInstance?: MonitorCriteriaInstance | undefined;
1275
+ }): Promise<string | null> {
1276
+ return Evaluator.buildKubernetesRootCauseContext({
1277
+ dataToProcess: metricResponse({
1278
+ kubernetesResourceBreakdown: {
1279
+ clusterName: "prod-cluster",
1280
+ metricName: input.metricName,
1281
+ metricFriendlyName: "Friendly Name",
1282
+ affectedResources: input.affectedResources,
1283
+ attributes: {},
1284
+ },
1285
+ }),
1286
+ monitorStep: input.monitorStep || kubernetesStep("k8s-node-not-ready"),
1287
+ monitor: new Monitor(),
1288
+ criteriaInstance: input.criteriaInstance,
1289
+ });
1290
+ }
1291
+
1292
+ describe("k8s-node-not-ready", () => {
1293
+ const nodeNotReadyStep: MonitorStep = kubernetesStep("k8s-node-not-ready");
1294
+
1295
+ test("the real firing criteria is an incident-opening `= 0` on the metric value", () => {
1296
+ /*
1297
+ * Guards the fixture: if the template stops firing on `= 0`, the
1298
+ * rest of this suite is testing nothing.
1299
+ */
1300
+ const criteria: MonitorCriteriaInstance =
1301
+ firingCriteria(nodeNotReadyStep);
1302
+ const filters: Array<CriteriaFilter> = criteria.data?.filters || [];
1303
+
1304
+ expect(filters.length).toBeGreaterThan(0);
1305
+ expect(filters[0]!.checkOn).toBe(CheckOn.MetricValue);
1306
+ expect(filters[0]!.filterType).toBe(FilterType.EqualTo);
1307
+ expect(filters[0]!.value).toBe(0);
1308
+ expect(criteria.data?.createIncidents).toBe(true);
1309
+ });
1310
+
1311
+ test("lists only the NotReady node and the analysis names it", async () => {
1312
+ const context: string | null = await kubernetesContext({
1313
+ metricName: "k8s.node.condition_ready",
1314
+ monitorStep: nodeNotReadyStep,
1315
+ criteriaInstance: firingCriteria(nodeNotReadyStep),
1316
+ affectedResources: [
1317
+ // The NotReady node. The old `> 0` filter removed exactly this row.
1318
+ { nodeName: "node-notready", metricValue: 0 },
1319
+ { nodeName: "node-ok-1", metricValue: 1 },
1320
+ ],
1321
+ });
1322
+
1323
+ expect(context).toContain(
1324
+ [
1325
+ "**Affected Resources** (1 total)",
1326
+ "",
1327
+ "1. **Node** `node-notready` — **0**",
1328
+ ].join("\n"),
1329
+ );
1330
+ expect(context).toContain(
1331
+ "Node `node-notready` is reporting NotReady (value: 0).",
1332
+ );
1333
+ expect(context).toContain("kubectl describe node node-notready");
1334
+ expect(context).not.toContain("node-ok-1");
1335
+ });
1336
+
1337
+ test("a node that was NotReady for only part of the window is still listed", async () => {
1338
+ /*
1339
+ * The worker's highest sample for a flapping node is 1; its lowest
1340
+ * is the 0 the Min-aggregated criteria actually fired on.
1341
+ */
1342
+ const context: string | null = await kubernetesContext({
1343
+ metricName: "k8s.node.condition_ready",
1344
+ monitorStep: nodeNotReadyStep,
1345
+ criteriaInstance: firingCriteria(nodeNotReadyStep),
1346
+ affectedResources: [
1347
+ { nodeName: "node-ok-1", metricValue: 1, lowestMetricValue: 1 },
1348
+ { nodeName: "node-flapping", metricValue: 1, lowestMetricValue: 0 },
1349
+ ],
1350
+ });
1351
+
1352
+ expect(context).toContain("**Affected Resources** (1 total)");
1353
+ expect(context).toContain("1. **Node** `node-flapping` — **0**");
1354
+ expect(context).toContain(
1355
+ "Node `node-flapping` is reporting NotReady (value: 0).",
1356
+ );
1357
+ expect(context).not.toContain("node-ok-1");
1358
+ });
1359
+
1360
+ test("every NotReady node survives the top-10 cap", async () => {
1361
+ const affectedResources: Array<KubernetesAffectedResource> = [
1362
+ { nodeName: "node-down-a", metricValue: 0 },
1363
+ { nodeName: "node-down-b", metricValue: 0 },
1364
+ ];
1365
+
1366
+ /*
1367
+ * Twelve Ready nodes — enough to overflow the ten-row slice if the
1368
+ * predicate ever stops filtering them out.
1369
+ */
1370
+ for (let i: number = 10; i < 22; i++) {
1371
+ affectedResources.push({ nodeName: `node-ok-${i}`, metricValue: 1 });
1372
+ }
1373
+
1374
+ const context: string | null = await kubernetesContext({
1375
+ metricName: "k8s.node.condition_ready",
1376
+ monitorStep: nodeNotReadyStep,
1377
+ criteriaInstance: firingCriteria(nodeNotReadyStep),
1378
+ affectedResources,
1379
+ });
1380
+
1381
+ expect(context).toContain("**Affected Resources** (2 total)");
1382
+ expect(context).toContain("`node-down-a`");
1383
+ expect(context).toContain("`node-down-b`");
1384
+ expect(context).not.toContain("node-ok-");
1385
+ expect(context).not.toContain("*... and");
1386
+ });
1387
+
1388
+ test("the recovery criteria (an upward comparison) keeps the `> 0` fallback", async () => {
1389
+ const context: string | null = await kubernetesContext({
1390
+ metricName: "k8s.node.condition_ready",
1391
+ monitorStep: nodeNotReadyStep,
1392
+ criteriaInstance: recoveryCriteria(nodeNotReadyStep),
1393
+ affectedResources: [
1394
+ { nodeName: "node-notready", metricValue: 0 },
1395
+ { nodeName: "node-ok-1", metricValue: 1 },
1396
+ ],
1397
+ });
1398
+
1399
+ expect(context).toContain("1. **Node** `node-ok-1` — **1**");
1400
+ expect(context).not.toContain("node-notready");
1401
+ });
1402
+ });
1403
+
1404
+ test("k8s-etcd-no-leader lists the leaderless member, not the healthy one", async () => {
1405
+ const etcdStep: MonitorStep = kubernetesStep("k8s-etcd-no-leader");
1406
+ const criteria: MonitorCriteriaInstance = firingCriteria(etcdStep);
1407
+
1408
+ // Fixture guard: the real criteria is an incident-opening `= 0`.
1409
+ expect(criteria.data?.filters[0]!.filterType).toBe(FilterType.EqualTo);
1410
+ expect(criteria.data?.filters[0]!.value).toBe(0);
1411
+ expect(criteria.data?.createIncidents).toBe(true);
1412
+
1413
+ const context: string | null = await kubernetesContext({
1414
+ metricName: "etcd_server_has_leader",
1415
+ monitorStep: etcdStep,
1416
+ criteriaInstance: criteria,
1417
+ affectedResources: [
1418
+ { podName: "etcd-cp-1", metricValue: 1 },
1419
+ { podName: "etcd-cp-2", metricValue: 0 },
1420
+ ],
1421
+ });
1422
+
1423
+ expect(context).toContain("**Affected Resources** (1 total)");
1424
+ expect(context).toContain("1. **Pod** `etcd-cp-2` — **0**");
1425
+ expect(context).toContain("Most affected pod: `etcd-cp-2`");
1426
+ expect(context).not.toContain("etcd-cp-1");
1427
+ });
1428
+
1429
+ test("an `= 0` RECOVERY criteria does NOT turn the all-clear into a list of zeroes", async () => {
1430
+ const replicaStep: MonitorStep = kubernetesStep(
1431
+ "k8s-deployment-replica-mismatch",
1432
+ );
1433
+ const criteria: MonitorCriteriaInstance = recoveryCriteria(replicaStep);
1434
+
1435
+ // Fixture guard: the real recovery is a non-incident `= 0`.
1436
+ expect(criteria.data?.filters[0]!.filterType).toBe(FilterType.EqualTo);
1437
+ expect(criteria.data?.filters[0]!.value).toBe(0);
1438
+ expect(criteria.data?.createIncidents).toBe(false);
1439
+ expect(criteria.data?.createAlerts).toBe(false);
1440
+
1441
+ const context: string | null = await kubernetesContext({
1442
+ metricName: "k8s.deployment.unavailable_replicas",
1443
+ monitorStep: replicaStep,
1444
+ criteriaInstance: criteria,
1445
+ affectedResources: [
1446
+ { workloadType: "Deployment", workloadName: "web", metricValue: 0 },
1447
+ { workloadType: "Deployment", workloadName: "api", metricValue: 0 },
1448
+ ],
1449
+ });
1450
+
1451
+ expect(context).toContain("**Kubernetes Cluster Details**");
1452
+ expect(context).not.toContain("**Affected Resources**");
1453
+ expect(context).not.toContain("**Root Cause Analysis**");
1454
+ });
1455
+
1456
+ test("a `> 0` criteria renders exactly as the hardcoded filter did, on the highest sample", async () => {
1457
+ const replicaStep: MonitorStep = kubernetesStep(
1458
+ "k8s-deployment-replica-mismatch",
1459
+ );
1460
+
1461
+ const context: string | null = await kubernetesContext({
1462
+ metricName: "k8s.deployment.unavailable_replicas",
1463
+ monitorStep: replicaStep,
1464
+ criteriaInstance: firingCriteria(replicaStep),
1465
+ affectedResources: [
1466
+ {
1467
+ workloadType: "Deployment",
1468
+ workloadName: "web",
1469
+ metricValue: 3,
1470
+ lowestMetricValue: 0,
1471
+ },
1472
+ { workloadType: "Deployment", workloadName: "api", metricValue: 5 },
1473
+ { workloadType: "Deployment", workloadName: "ok", metricValue: 0 },
1474
+ ],
1475
+ });
1476
+
1477
+ expect(context).toContain("**Affected Resources** (2 total)");
1478
+ expect(context).toContain(
1479
+ [
1480
+ "1. **Deployment** `api` — **5**",
1481
+ "2. **Deployment** `web` — **3**",
1482
+ ].join("\n"),
1483
+ );
1484
+ expect(context).not.toContain("`ok`");
1485
+
1486
+ // Still worst-HIGHEST-first for an upward comparison.
1487
+ const apiIndex: number = context!.indexOf("`api`");
1488
+ const webIndex: number = context!.indexOf("`web`");
1489
+ expect(apiIndex).toBeGreaterThan(-1);
1490
+ expect(webIndex).toBeGreaterThan(apiIndex);
1491
+ expect(context).toContain(
1492
+ "Deployment `api` has **5** unavailable replica(s).",
1493
+ );
1494
+ });
1495
+
1496
+ test("a `<` criteria keeps the breaching rows and sorts worst (lowest) first", async () => {
1497
+ const context: string | null = await kubernetesContext({
1498
+ metricName: "k8s.deployment.available",
1499
+ criteriaInstance: syntheticCriteria({
1500
+ opensIncident: true,
1501
+ filters: [
1502
+ {
1503
+ checkOn: CheckOn.MetricValue,
1504
+ filterType: FilterType.LessThan,
1505
+ value: 2,
1506
+ },
1507
+ ],
1508
+ }),
1509
+ affectedResources: [
1510
+ { workloadType: "Deployment", workloadName: "mild", metricValue: 1 },
1511
+ { workloadType: "Deployment", workloadName: "fine", metricValue: 3 },
1512
+ { workloadType: "Deployment", workloadName: "worst", metricValue: 0 },
1513
+ ],
1514
+ });
1515
+
1516
+ expect(context).toContain("**Affected Resources** (2 total)");
1517
+ expect(context).not.toContain("`fine`");
1518
+
1519
+ const worstIndex: number = context!.indexOf("`worst`");
1520
+ const mildIndex: number = context!.indexOf("`mild`");
1521
+ expect(worstIndex).toBeGreaterThan(-1);
1522
+ expect(mildIndex).toBeGreaterThan(worstIndex);
1523
+ });
1524
+
1525
+ test("a firing equality ABOVE the floor keeps the `> 0` fallback", async () => {
1526
+ /*
1527
+ * `= 1` on a 0/1 pressure condition fires when the metric RISES, so
1528
+ * it must not be read as a fall.
1529
+ */
1530
+ const context: string | null = await kubernetesContext({
1531
+ metricName: "k8s.node.condition_memory_pressure",
1532
+ criteriaInstance: syntheticCriteria({
1533
+ opensIncident: true,
1534
+ filters: [
1535
+ {
1536
+ checkOn: CheckOn.MetricValue,
1537
+ filterType: FilterType.EqualTo,
1538
+ value: 1,
1539
+ },
1540
+ ],
1541
+ }),
1542
+ affectedResources: [
1543
+ { nodeName: "node-pressured", metricValue: 1, lowestMetricValue: 0 },
1544
+ { nodeName: "node-ok-1", metricValue: 0 },
1545
+ ],
1546
+ });
1547
+
1548
+ expect(context).toContain("1. **Node** `node-pressured` — **1**");
1549
+ expect(context).not.toContain("node-ok-1");
1550
+ });
1551
+
1552
+ test("no criteriaInstance falls back to dropping zero rows", async () => {
1553
+ const context: string | null = await kubernetesContext({
1554
+ metricName: "k8s.node.condition_ready",
1555
+ affectedResources: [
1556
+ { nodeName: "node-notready", metricValue: 0 },
1557
+ { nodeName: "node-ok-1", metricValue: 1 },
1558
+ ],
1559
+ });
1560
+
1561
+ expect(context).toContain("1. **Node** `node-ok-1` — **1**");
1562
+ expect(context).not.toContain("node-notready");
1563
+ });
1159
1564
  });
1160
1565
 
1161
1566
  /*