@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
@@ -57,27 +57,30 @@ const getTile: GetTileFunction = (id: string): HTMLElement => {
57
57
  };
58
58
 
59
59
  describe("SloBudgetBar", () => {
60
- test("before the first evaluation it draws only an empty track", () => {
61
- render(
62
- <SloBudgetBar
63
- errorBudgetRemainingPercentage={null}
64
- atRiskThresholdPercentage={20}
65
- />,
66
- );
67
-
68
- const bar: HTMLElement = screen.getByRole("progressbar", {
69
- name: "Error budget remaining",
70
- });
71
-
72
- expect(bar).not.toHaveAttribute("aria-valuenow");
73
- expect(bar).toHaveAttribute(
74
- "aria-valuetext",
75
- "Error budget not evaluated yet",
76
- );
77
- expect(bar).toHaveClass("bg-gray-100");
78
- expect(screen.queryByTestId("slo-budget-bar-fill")).toBeNull();
79
- expect(screen.queryByTestId("slo-budget-bar-marker")).toBeNull();
80
- });
60
+ test.each([undefined, null, NaN, Infinity, -Infinity])(
61
+ "an unknown budget (%s) draws only an empty track",
62
+ (remainingPercentage: number | undefined | null) => {
63
+ render(
64
+ <SloBudgetBar
65
+ errorBudgetRemainingPercentage={remainingPercentage}
66
+ atRiskThresholdPercentage={20}
67
+ />,
68
+ );
69
+
70
+ const bar: HTMLElement = screen.getByRole("progressbar", {
71
+ name: "Error budget remaining",
72
+ });
73
+
74
+ expect(bar).not.toHaveAttribute("aria-valuenow");
75
+ expect(bar).toHaveAttribute(
76
+ "aria-valuetext",
77
+ "Error budget not evaluated yet",
78
+ );
79
+ expect(bar).toHaveClass("bg-gray-100");
80
+ expect(screen.queryByTestId("slo-budget-bar-fill")).toBeNull();
81
+ expect(screen.queryByTestId("slo-budget-bar-marker")).toBeNull();
82
+ },
83
+ );
81
84
 
82
85
  test("fills to the remaining budget and ticks the SLO's own at-risk threshold", () => {
83
86
  render(
@@ -117,27 +120,56 @@ describe("SloBudgetBar", () => {
117
120
  );
118
121
  });
119
122
 
120
- test("an overspent budget draws no fill on a red track, and says it is overspent", () => {
121
- render(
122
- <SloBudgetBar
123
- errorBudgetRemainingPercentage={-340}
124
- atRiskThresholdPercentage={20}
125
- />,
126
- );
127
-
128
- const bar: HTMLElement = screen.getByRole("progressbar");
129
-
130
- expect(screen.queryByTestId("slo-budget-bar-fill")).toBeNull();
131
- expect(bar).toHaveClass("bg-red-100");
132
- expect(bar).toHaveAttribute("aria-valuenow", "0");
133
- expect(bar.getAttribute("aria-valuetext")).toContain(
134
- "Error budget overspent (-340%)",
135
- );
136
- // The threshold tick still shows where "at risk" would have been.
137
- expect(screen.getByTestId("slo-budget-bar-marker")).toHaveStyle({
138
- left: "20%",
139
- });
140
- });
123
+ test.each([-340, -99882.8, -0.00001])(
124
+ "an exceeded budget (%s) draws no fill and describes the breach without a negative percentage",
125
+ (remainingPercentage: number) => {
126
+ render(
127
+ <SloBudgetBar
128
+ errorBudgetRemainingPercentage={remainingPercentage}
129
+ atRiskThresholdPercentage={20}
130
+ />,
131
+ );
132
+
133
+ const bar: HTMLElement = screen.getByRole("progressbar");
134
+
135
+ expect(screen.queryByTestId("slo-budget-bar-fill")).toBeNull();
136
+ expect(bar).toHaveClass("bg-red-100");
137
+ expect(bar).toHaveAttribute("aria-valuenow", "0");
138
+ expect(bar).toHaveAttribute(
139
+ "aria-valuetext",
140
+ "Error budget exceeded; no budget remaining; at risk at 20% or less",
141
+ );
142
+ // The threshold tick still shows where "at risk" would have been.
143
+ expect(screen.getByTestId("slo-budget-bar-marker")).toHaveStyle({
144
+ left: "20%",
145
+ });
146
+ },
147
+ );
148
+
149
+ test.each([0, -0])(
150
+ "an exactly exhausted budget (%s) is distinct from an exceeded budget",
151
+ (remainingPercentage: number) => {
152
+ render(
153
+ <SloBudgetBar
154
+ errorBudgetRemainingPercentage={remainingPercentage}
155
+ atRiskThresholdPercentage={20}
156
+ />,
157
+ );
158
+
159
+ const bar: HTMLElement = screen.getByRole("progressbar");
160
+
161
+ expect(bar).toHaveClass("bg-red-100");
162
+ expect(bar).toHaveAttribute("aria-valuenow", "0");
163
+ expect(bar).toHaveAttribute(
164
+ "aria-valuetext",
165
+ "No error budget remaining; at risk at 20% or less",
166
+ );
167
+ expect(screen.queryByTestId("slo-budget-bar-fill")).toBeNull();
168
+ expect(screen.getByTestId("slo-budget-bar-marker")).toHaveStyle({
169
+ left: "20%",
170
+ });
171
+ },
172
+ );
141
173
 
142
174
  test("clamps the fill and the marker onto the track", () => {
143
175
  render(
@@ -153,6 +185,10 @@ describe("SloBudgetBar", () => {
153
185
  expect(screen.getByTestId("slo-budget-bar-marker")).toHaveStyle({
154
186
  left: "100%",
155
187
  });
188
+ expect(screen.getByRole("progressbar")).toHaveAttribute(
189
+ "aria-valuetext",
190
+ "100% of the error budget remaining; at risk at 150% or less",
191
+ );
156
192
  });
157
193
  });
158
194
 
@@ -362,9 +398,11 @@ describe("SloKpiStrip", () => {
362
398
  );
363
399
 
364
400
  const budget: HTMLElement = getTile("slo-kpi-budget");
365
- expect(within(budget).getByText("-120%")).toHaveClass("text-red-700");
401
+ expect(within(budget).getByText("Budget exceeded")).toHaveClass(
402
+ "text-red-700",
403
+ );
366
404
  expect(
367
- within(budget).getByText("−12m 30s over budget of 43m 12s"),
405
+ within(budget).getByText("12m 30s over budget · 43m 12s allowed"),
368
406
  ).toBeInTheDocument();
369
407
  expect(within(budget).queryByTestId("slo-budget-bar-fill")).toBeNull();
370
408
  expect(within(budget).getByRole("progressbar")).toHaveClass("bg-red-100");
@@ -374,6 +412,267 @@ describe("SloKpiStrip", () => {
374
412
  expect(within(runway).getByText("12m 30s over budget")).toBeInTheDocument();
375
413
  });
376
414
 
415
+ test("the reported huge negative percentage becomes a clear breach and a duration in a young window", () => {
416
+ const slo: ServiceLevelObjective = buildSlo({
417
+ currentSliPercentage: 0.0172,
418
+ errorBudgetRemainingPercentage: -99882.8,
419
+ errorBudgetRemainingSeconds: -346980,
420
+ errorBudgetTotalSeconds: 346980 / 998.828,
421
+ currentBurnRate: 1000,
422
+ sloStatus: SloStatus.BudgetExhausted,
423
+ lastEvaluatedAt: NOW,
424
+ });
425
+
426
+ render(
427
+ <SloKpiStrip slo={slo} lowestEnabledBurnRateThreshold={14.4} now={NOW} />,
428
+ );
429
+
430
+ const budget: HTMLElement = getTile("slo-kpi-budget");
431
+ expect(within(budget).getByText("Budget exceeded")).toHaveClass(
432
+ "text-red-700",
433
+ );
434
+ expect(
435
+ within(budget).getByText(
436
+ "4d 23m over budget · 5m 47s allowed · window 13% full",
437
+ ),
438
+ ).toBeInTheDocument();
439
+ expect(budget).not.toHaveTextContent("-99882.8%");
440
+ expect(budget).not.toHaveTextContent("−4d");
441
+ expect(within(budget).queryByTestId("slo-budget-bar-fill")).toBeNull();
442
+ expect(within(budget).getByRole("progressbar")).toHaveAttribute(
443
+ "aria-valuetext",
444
+ "Error budget exceeded; no budget remaining; at risk at 20% or less",
445
+ );
446
+ expect(within(budget).getByTestId("slo-budget-bar-marker")).toHaveStyle({
447
+ left: "20%",
448
+ });
449
+
450
+ // Presentation must not erase the signed values used by calculations.
451
+ expect(slo.errorBudgetRemainingPercentage).toBe(-99882.8);
452
+ expect(slo.errorBudgetRemainingSeconds).toBe(-346980);
453
+ expect(slo.errorBudgetTotalSeconds).toBe(346980 / 998.828);
454
+ expect(
455
+ within(getTile("slo-kpi-runway")).getByText("Overspent"),
456
+ ).toBeInTheDocument();
457
+ });
458
+
459
+ test("even a fractional overage remains a breach instead of rounding to zero", () => {
460
+ render(
461
+ <SloKpiStrip
462
+ slo={buildSlo({
463
+ errorBudgetRemainingPercentage: -0.00001,
464
+ errorBudgetRemainingSeconds: -0.25,
465
+ errorBudgetTotalSeconds: 2592,
466
+ lastEvaluatedAt: NOW,
467
+ })}
468
+ lowestEnabledBurnRateThreshold={null}
469
+ now={NOW}
470
+ />,
471
+ );
472
+
473
+ const budget: HTMLElement = getTile("slo-kpi-budget");
474
+ expect(within(budget).getByText("Budget exceeded")).toHaveClass(
475
+ "text-red-700",
476
+ );
477
+ expect(
478
+ within(budget).getByText("under 1s over budget · 43m 12s allowed"),
479
+ ).toBeInTheDocument();
480
+ expect(within(budget).queryByText("0%")).toBeNull();
481
+ expect(within(budget).queryByText("No budget left")).toBeNull();
482
+ expect(within(budget).queryByTestId("slo-budget-bar-fill")).toBeNull();
483
+ });
484
+
485
+ test.each([0, -0])(
486
+ "zero remaining (%s) says the budget is used up without claiming an overage",
487
+ (remaining: number) => {
488
+ render(
489
+ <SloKpiStrip
490
+ slo={buildSlo({
491
+ errorBudgetRemainingPercentage: remaining,
492
+ errorBudgetRemainingSeconds: remaining,
493
+ errorBudgetTotalSeconds: 2592,
494
+ lastEvaluatedAt: NOW,
495
+ })}
496
+ lowestEnabledBurnRateThreshold={null}
497
+ now={NOW}
498
+ />,
499
+ );
500
+
501
+ const budget: HTMLElement = getTile("slo-kpi-budget");
502
+ expect(within(budget).getByText("No budget left")).toHaveClass(
503
+ "text-red-700",
504
+ );
505
+ expect(
506
+ within(budget).getByText("0s left of 43m 12s"),
507
+ ).toBeInTheDocument();
508
+ expect(within(budget).queryByText("Budget exceeded")).toBeNull();
509
+ expect(within(budget).queryByTestId("slo-budget-bar-fill")).toBeNull();
510
+ expect(within(budget).getByRole("progressbar")).toHaveAttribute(
511
+ "aria-valuetext",
512
+ "No error budget remaining; at risk at 20% or less",
513
+ );
514
+ },
515
+ );
516
+
517
+ test.each([undefined, null, NaN, Infinity, -Infinity])(
518
+ "unknown remaining percentage (%s) is not mistaken for a healthy or exhausted budget",
519
+ (remainingPercentage: number | undefined | null) => {
520
+ const slo: ServiceLevelObjective = buildSlo({
521
+ errorBudgetTotalSeconds: 648,
522
+ sloStatus: SloStatus.Healthy,
523
+ lastEvaluatedAt: NOW,
524
+ });
525
+ Object.assign(slo, {
526
+ errorBudgetRemainingPercentage: remainingPercentage,
527
+ });
528
+
529
+ render(
530
+ <SloKpiStrip
531
+ slo={slo}
532
+ lowestEnabledBurnRateThreshold={null}
533
+ now={NOW}
534
+ />,
535
+ );
536
+
537
+ const budget: HTMLElement = getTile("slo-kpi-budget");
538
+ expect(within(budget).getByText("—")).toHaveClass("text-gray-400");
539
+ expect(within(budget).getByText("Not evaluated yet")).toBeInTheDocument();
540
+ expect(budget).not.toHaveTextContent(/NaN|Infinity|window .* full/);
541
+ expect(within(budget).queryByText("Budget exceeded")).toBeNull();
542
+ expect(within(budget).queryByText("No budget left")).toBeNull();
543
+ expect(within(budget).queryByTestId("slo-budget-bar-fill")).toBeNull();
544
+ expect(within(budget).queryByTestId("slo-budget-bar-marker")).toBeNull();
545
+ expect(within(budget).getByRole("progressbar")).not.toHaveAttribute(
546
+ "aria-valuenow",
547
+ );
548
+ },
549
+ );
550
+
551
+ test.each([
552
+ { percentage: 0.00001, label: "<0.1%", color: "text-amber-700" },
553
+ { percentage: 15.555, label: "15.6%", color: "text-amber-700" },
554
+ { percentage: 20, label: "20%", color: "text-amber-700" },
555
+ { percentage: 20.04, label: "20%", color: "text-emerald-700" },
556
+ { percentage: 100.4, label: "100%", color: "text-emerald-700" },
557
+ ])(
558
+ "$percentage percent has a readable $label headline while preserving its actual health and bar fill",
559
+ ({
560
+ percentage,
561
+ label,
562
+ color,
563
+ }: {
564
+ percentage: number;
565
+ label: string;
566
+ color: string;
567
+ }) => {
568
+ const slo: ServiceLevelObjective = buildSlo({
569
+ errorBudgetRemainingPercentage: percentage,
570
+ errorBudgetRemainingSeconds: (percentage * 2592) / 100,
571
+ errorBudgetTotalSeconds: 2592,
572
+ lastEvaluatedAt: NOW,
573
+ });
574
+
575
+ render(
576
+ <SloKpiStrip
577
+ slo={slo}
578
+ lowestEnabledBurnRateThreshold={null}
579
+ now={NOW}
580
+ />,
581
+ );
582
+
583
+ const budget: HTMLElement = getTile("slo-kpi-budget");
584
+ expect(within(budget).getByText(label)).toHaveClass(color);
585
+ expect(within(budget).getByTestId("slo-budget-bar-fill")).toHaveStyle({
586
+ width: `${Math.min(percentage, 100)}%`,
587
+ });
588
+ expect(within(budget).getByRole("progressbar")).toHaveAttribute(
589
+ "aria-valuetext",
590
+ `${label} of the error budget remaining; at risk at 20% or less`,
591
+ );
592
+ expect(within(budget).queryByText("No budget left")).toBeNull();
593
+ expect(slo.errorBudgetRemainingPercentage).toBe(percentage);
594
+ },
595
+ );
596
+
597
+ test.each([
598
+ {
599
+ percentage: -10,
600
+ headline: "Budget exceeded",
601
+ description: "Allowed downtime has been exceeded",
602
+ },
603
+ {
604
+ percentage: 0,
605
+ headline: "No budget left",
606
+ description: "All allowed downtime has been used",
607
+ },
608
+ {
609
+ percentage: 50,
610
+ headline: "50%",
611
+ description: "Remaining share of the allowed downtime",
612
+ },
613
+ ])(
614
+ "a known $headline state remains explained when the duration is missing",
615
+ ({
616
+ percentage,
617
+ headline,
618
+ description,
619
+ }: {
620
+ percentage: number;
621
+ headline: string;
622
+ description: string;
623
+ }) => {
624
+ const slo: ServiceLevelObjective = buildSlo({
625
+ errorBudgetRemainingPercentage: percentage,
626
+ errorBudgetTotalSeconds: 2592,
627
+ lastEvaluatedAt: NOW,
628
+ });
629
+
630
+ render(
631
+ <SloKpiStrip
632
+ slo={slo}
633
+ lowestEnabledBurnRateThreshold={null}
634
+ now={NOW}
635
+ />,
636
+ );
637
+
638
+ const budget: HTMLElement = getTile("slo-kpi-budget");
639
+ expect(within(budget).getByText(headline)).toBeInTheDocument();
640
+ expect(within(budget).getByText(description)).toBeInTheDocument();
641
+ expect(within(budget).queryByText("Not evaluated yet")).toBeNull();
642
+ expect(budget).not.toHaveTextContent(/NaN|Infinity/);
643
+ },
644
+ );
645
+
646
+ test.each([undefined, NaN, 0])(
647
+ "an exceeded budget with no usable total (%s) still explains the measured overage",
648
+ (totalSeconds: number | undefined) => {
649
+ render(
650
+ <SloKpiStrip
651
+ slo={buildSlo({
652
+ errorBudgetRemainingPercentage: -10,
653
+ errorBudgetRemainingSeconds: -750,
654
+ // exactOptionalPropertyTypes: "no total" is an absent key, not an explicit undefined.
655
+ ...(totalSeconds === undefined
656
+ ? {}
657
+ : { errorBudgetTotalSeconds: totalSeconds }),
658
+ lastEvaluatedAt: NOW,
659
+ })}
660
+ lowestEnabledBurnRateThreshold={null}
661
+ now={NOW}
662
+ />,
663
+ );
664
+
665
+ const budget: HTMLElement = getTile("slo-kpi-budget");
666
+ expect(within(budget).getByText("Budget exceeded")).toBeInTheDocument();
667
+ expect(
668
+ within(budget).getByText("12m 30s over budget"),
669
+ ).toBeInTheDocument();
670
+ expect(budget).not.toHaveTextContent(
671
+ /allowed|NaN|Infinity|window .* full/,
672
+ );
673
+ },
674
+ );
675
+
377
676
  test("a young rolling window says so under the budget that is still growing", () => {
378
677
  render(
379
678
  <SloKpiStrip
@@ -398,6 +697,51 @@ describe("SloKpiStrip", () => {
398
697
  ).toBeInTheDocument();
399
698
  });
400
699
 
700
+ test.each([
701
+ {
702
+ label: "calendar month",
703
+ windowType: SloWindowType.CalendarMonth,
704
+ multiMonitorMode: SloMultiMonitorMode.AnyDown,
705
+ },
706
+ {
707
+ label: "averaged monitor downtime",
708
+ windowType: SloWindowType.Rolling,
709
+ multiMonitorMode: SloMultiMonitorMode.MonitorSecondsAverage,
710
+ },
711
+ ])(
712
+ "an exceeded budget for $label does not add a misleading rolling-window fill estimate",
713
+ ({
714
+ windowType,
715
+ multiMonitorMode,
716
+ }: {
717
+ label: string;
718
+ windowType: SloWindowType;
719
+ multiMonitorMode: SloMultiMonitorMode;
720
+ }) => {
721
+ render(
722
+ <SloKpiStrip
723
+ slo={buildSlo({
724
+ windowType,
725
+ multiMonitorMode,
726
+ errorBudgetRemainingPercentage: -100,
727
+ errorBudgetRemainingSeconds: -648,
728
+ errorBudgetTotalSeconds: 648,
729
+ lastEvaluatedAt: NOW,
730
+ })}
731
+ lowestEnabledBurnRateThreshold={null}
732
+ now={NOW}
733
+ />,
734
+ );
735
+
736
+ const budget: HTMLElement = getTile("slo-kpi-budget");
737
+ expect(within(budget).getByText("Budget exceeded")).toBeInTheDocument();
738
+ expect(
739
+ within(budget).getByText("10m 48s over budget · 10m 48s allowed"),
740
+ ).toBeInTheDocument();
741
+ expect(budget).not.toHaveTextContent("window 25% full");
742
+ },
743
+ );
744
+
401
745
  test("Monitor Seconds Average SLOs show no runway rather than a wrong one", () => {
402
746
  render(
403
747
  <SloKpiStrip