@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.
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/DatabaseService.ts +10 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/MonitorService.ts +3 -2
- package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
- package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
- package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
- package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
- package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
- package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
- package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
- package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
- package/Server/Utils/Monitor/RootCauseList.ts +139 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
- package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
- package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
- package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
- package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
- package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
- package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
- package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
- package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
- package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
- package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
- package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
- package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
- package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
- package/Tests/Server/Types/Markdown.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
- package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
- package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
- package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
- package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
- package/Tests/Types/API/Route.test.ts +56 -0
- package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
- package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
- package/Tests/UI/Components/Icon.test.tsx +26 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
- package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
- package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
- package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
- package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
- package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
- package/Types/API/Route.ts +19 -2
- package/Types/API/URL.ts +26 -2
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
- package/Types/Permission.ts +90 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/UI/Components/Feed/FeedOptions.ts +427 -0
- package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
- package/UI/Components/Feed/useFeedItems.ts +28 -9
- package/UI/Components/Feed/useFeedOptions.ts +225 -0
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/Utils/LabelRuleImportExport.ts +27 -9
- package/Utils/Monitor/MetricValueFormatter.ts +2 -2
- package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
- package/Utils/Slo/SloDuration.ts +22 -6
- package/Utils/Slo/SloHealth.ts +8 -7
- package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
- package/Utils/Slo/SloOverviewText.ts +33 -0
- package/Utils/Slo/SloProjection.ts +4 -3
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +7 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +3 -2
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
- package/build/dist/Types/API/Route.js +18 -2
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/API/URL.js +23 -2
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
- package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
- package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
- package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
- package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/Utils/LabelRuleImportExport.js +22 -6
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
- package/build/dist/Utils/Slo/SloDuration.js +13 -5
- package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
- package/build/dist/Utils/Slo/SloHealth.js +8 -7
- package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
- package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
- package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
- package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,19 +32,25 @@ import { describe, expect, test } from "@jest/globals";
|
|
|
32
32
|
* run it through Markdown.convertToHTML for the email body.
|
|
33
33
|
*
|
|
34
34
|
* Before this suite existed, nothing anywhere pinned the Breaching
|
|
35
|
-
* Samples table or the "- Unit:" line, and both
|
|
36
|
-
* the exporter's raw UCUM code glued on: a memory
|
|
37
|
-
* dashboard drew as "1.07 GB" arrived in the inbox as
|
|
38
|
-
* and a ratio metric's Value column read "0.06 1".
|
|
35
|
+
* Samples block (a table back then) or the "- Unit:" line, and both
|
|
36
|
+
* rendered raw digits with the exporter's raw UCUM code glued on: a memory
|
|
37
|
+
* breach that the dashboard drew as "1.07 GB" arrived in the inbox as
|
|
38
|
+
* "1073741824 By", and a ratio metric's Value column read "0.06 1".
|
|
39
39
|
*
|
|
40
40
|
* These tests pin:
|
|
41
|
-
* -
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* - each sample's value at human scale, in the unit that sample landed
|
|
42
|
+
* on,
|
|
43
|
+
* - formula components each in their OWN unit, labelled by alias alone
|
|
44
|
+
* (auto-scaling means neighbouring samples disagree with any one
|
|
45
|
+
* configured unit),
|
|
44
46
|
* - the "- Unit:" line spelled out, and dropped when there is no unit,
|
|
45
47
|
* - the formula guard: a formula expression is not a metric name,
|
|
46
|
-
* - that the
|
|
47
|
-
* email render the same sample identically.
|
|
48
|
+
* - that the Breaching Samples list and the "Filter Conditions Met"
|
|
49
|
+
* sentence in the same email render the same sample identically.
|
|
50
|
+
*
|
|
51
|
+
* The list's own shape — its markdown, and how it parses and renders in
|
|
52
|
+
* the email, Slack and plain text — is pinned in
|
|
53
|
+
* MonitorCriteriaEvaluatorBreachingSamples.test.ts.
|
|
48
54
|
*/
|
|
49
55
|
|
|
50
56
|
type EvaluatorPrivate = {
|
|
@@ -115,33 +121,57 @@ function rootCause(ctx: MetricCriteriaContext): string {
|
|
|
115
121
|
return context as string;
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
/*
|
|
125
|
+
* The Breaching Samples list item of the sample taken at BREACH_TIME: its
|
|
126
|
+
* first line ("1. `<timestamp>` — **<value>**"), the value on it, and the
|
|
127
|
+
* detail bullets nested under it.
|
|
128
|
+
*/
|
|
129
|
+
const LIST_ITEM_LINE: RegExp = /^\d+\. /;
|
|
130
|
+
const DETAIL_LINE: RegExp = /^ {3,4}- /;
|
|
131
|
+
|
|
132
|
+
function sampleItemLines(context: string): Array<string> {
|
|
133
|
+
const lines: Array<string> = context.split("\n");
|
|
134
|
+
const index: number = lines.findIndex((line: string) => {
|
|
135
|
+
return LIST_ITEM_LINE.test(line) && line.includes(BREACH_TIME_ISO);
|
|
122
136
|
});
|
|
123
137
|
|
|
124
|
-
expect(
|
|
125
|
-
|
|
138
|
+
expect(index).toBeGreaterThan(-1);
|
|
139
|
+
|
|
140
|
+
const itemLines: Array<string> = [lines[index] as string];
|
|
141
|
+
|
|
142
|
+
for (const line of lines.slice(index + 1)) {
|
|
143
|
+
if (!DETAIL_LINE.test(line)) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
itemLines.push(line);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return itemLines;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return line.startsWith("| Timestamp |");
|
|
132
|
-
});
|
|
153
|
+
function sampleItem(context: string): string {
|
|
154
|
+
return sampleItemLines(context)[0] as string;
|
|
155
|
+
}
|
|
133
156
|
|
|
134
|
-
|
|
135
|
-
|
|
157
|
+
function sampleValue(context: string): string {
|
|
158
|
+
const match: RegExpMatchArray | null =
|
|
159
|
+
sampleItem(context).match(/ — \*\*(.*)\*\*$/);
|
|
160
|
+
|
|
161
|
+
expect(match).not.toBeNull();
|
|
162
|
+
return (match as RegExpMatchArray)[1] as string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sampleDetails(context: string): Array<string> {
|
|
166
|
+
return sampleItemLines(context).slice(1);
|
|
136
167
|
}
|
|
137
168
|
|
|
138
|
-
describe("MonitorCriteriaEvaluator - Breaching Samples
|
|
169
|
+
describe("MonitorCriteriaEvaluator - Breaching Samples values", () => {
|
|
139
170
|
test("renders bytes at human scale instead of raw digits", () => {
|
|
140
171
|
const context: string = rootCause(makeContext({ unit: "By" }));
|
|
141
172
|
|
|
142
|
-
expect(
|
|
143
|
-
|
|
144
|
-
);
|
|
173
|
+
expect(sampleItem(context)).toBe(`1. \`${BREACH_TIME_ISO}\` — **1.07 GB**`);
|
|
174
|
+
expect(sampleDetails(context)).toEqual([]);
|
|
145
175
|
expect(context).not.toContain("1073741824");
|
|
146
176
|
});
|
|
147
177
|
|
|
@@ -156,7 +186,7 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
156
186
|
}),
|
|
157
187
|
);
|
|
158
188
|
|
|
159
|
-
expect(
|
|
189
|
+
expect(sampleValue(context)).toBe("1.5 sec");
|
|
160
190
|
});
|
|
161
191
|
|
|
162
192
|
test("renders a value the user typed a GB threshold against in GB terms", () => {
|
|
@@ -175,10 +205,10 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
175
205
|
}),
|
|
176
206
|
);
|
|
177
207
|
|
|
178
|
-
expect(
|
|
208
|
+
expect(sampleValue(context)).toBe("2.5 TB");
|
|
179
209
|
});
|
|
180
210
|
|
|
181
|
-
test("each
|
|
211
|
+
test("each sample names the unit IT landed on", () => {
|
|
182
212
|
const context: string = rootCause(
|
|
183
213
|
makeContext({
|
|
184
214
|
unit: "By",
|
|
@@ -194,12 +224,12 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
194
224
|
);
|
|
195
225
|
|
|
196
226
|
/*
|
|
197
|
-
* 922 KB and 1.26 MB in the same
|
|
198
|
-
* rides each
|
|
199
|
-
*
|
|
227
|
+
* 922 KB and 1.26 MB in the same list. This is exactly why the unit
|
|
228
|
+
* rides each value instead of a heading — no single unit could
|
|
229
|
+
* describe both samples.
|
|
200
230
|
*/
|
|
201
|
-
expect(context).toContain("
|
|
202
|
-
expect(context).toContain(
|
|
231
|
+
expect(context).toContain("1. `2026-08-14T10:29:00.000Z` — **922 KB**");
|
|
232
|
+
expect(context).toContain(`2. \`${BREACH_TIME_ISO}\` — **1.26 MB**`);
|
|
203
233
|
});
|
|
204
234
|
|
|
205
235
|
test("a ratio metric's fraction is shown as the percentage it means", () => {
|
|
@@ -213,9 +243,9 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
213
243
|
}),
|
|
214
244
|
);
|
|
215
245
|
|
|
216
|
-
expect(
|
|
246
|
+
expect(sampleValue(context)).toBe("5.85%");
|
|
217
247
|
// The old rendering: a bare "0.06", which reads as 0.06%.
|
|
218
|
-
expect(context).not.toContain("
|
|
248
|
+
expect(context).not.toContain("**0.06**");
|
|
219
249
|
expect(context).not.toContain("0.06 1");
|
|
220
250
|
});
|
|
221
251
|
|
|
@@ -230,14 +260,14 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
230
260
|
}),
|
|
231
261
|
);
|
|
232
262
|
|
|
233
|
-
expect(
|
|
263
|
+
expect(sampleValue(context)).toBe("512");
|
|
234
264
|
expect(context).not.toContain("{thread}");
|
|
235
265
|
});
|
|
236
266
|
|
|
237
267
|
/*
|
|
238
268
|
* BACKWARD COMPATIBILITY. A metric with no unit must keep every digit —
|
|
239
269
|
* a counter at 5000 is not "5K" in an alert, because the reader may be
|
|
240
|
-
* scanning the
|
|
270
|
+
* scanning the list for the exact value that crossed the threshold.
|
|
241
271
|
*/
|
|
242
272
|
test("a unitless value keeps its exact digits", () => {
|
|
243
273
|
const context: string = rootCause(
|
|
@@ -250,7 +280,7 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
250
280
|
}),
|
|
251
281
|
);
|
|
252
282
|
|
|
253
|
-
expect(
|
|
283
|
+
expect(sampleValue(context)).toBe("5000");
|
|
254
284
|
expect(context).not.toContain("5K");
|
|
255
285
|
});
|
|
256
286
|
|
|
@@ -264,7 +294,7 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
264
294
|
}),
|
|
265
295
|
);
|
|
266
296
|
|
|
267
|
-
expect(
|
|
297
|
+
expect(sampleValue(context)).toBe("2.33");
|
|
268
298
|
});
|
|
269
299
|
|
|
270
300
|
/*
|
|
@@ -282,11 +312,11 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
282
312
|
}),
|
|
283
313
|
);
|
|
284
314
|
|
|
285
|
-
expect(
|
|
315
|
+
expect(sampleValue(context)).toBe("Infinity");
|
|
286
316
|
expect(context).not.toContain("InfinityP");
|
|
287
317
|
});
|
|
288
318
|
|
|
289
|
-
test("
|
|
319
|
+
test("attributes are listed under the sample, unaffected by the unit", () => {
|
|
290
320
|
const context: string = rootCause(
|
|
291
321
|
makeContext({
|
|
292
322
|
unit: "By",
|
|
@@ -301,14 +331,12 @@ describe("MonitorCriteriaEvaluator - Breaching Samples value column", () => {
|
|
|
301
331
|
}),
|
|
302
332
|
);
|
|
303
333
|
|
|
304
|
-
expect(
|
|
305
|
-
|
|
306
|
-
);
|
|
307
|
-
expect(sampleRow(context)).toContain("| web-1 |");
|
|
334
|
+
expect(sampleItem(context)).toBe(`1. \`${BREACH_TIME_ISO}\` — **1.07 GB**`);
|
|
335
|
+
expect(sampleDetails(context)).toEqual([" - `k8s.pod.name`: `web-1`"]);
|
|
308
336
|
});
|
|
309
337
|
});
|
|
310
338
|
|
|
311
|
-
describe("MonitorCriteriaEvaluator - Breaching Samples
|
|
339
|
+
describe("MonitorCriteriaEvaluator - Breaching Samples formula components", () => {
|
|
312
340
|
function formulaContext(): MetricCriteriaContext {
|
|
313
341
|
return makeContext({
|
|
314
342
|
metricName: "a + b",
|
|
@@ -344,27 +372,27 @@ describe("MonitorCriteriaEvaluator - Breaching Samples component columns", () =>
|
|
|
344
372
|
});
|
|
345
373
|
}
|
|
346
374
|
|
|
347
|
-
test("each component
|
|
375
|
+
test("each component is formatted in its own unit", () => {
|
|
348
376
|
const context: string = rootCause(formulaContext());
|
|
349
377
|
|
|
350
|
-
expect(
|
|
351
|
-
|
|
352
|
-
|
|
378
|
+
expect(sampleItemLines(context)).toEqual([
|
|
379
|
+
`1. \`${BREACH_TIME_ISO}\` — **1.07 GB**`,
|
|
380
|
+
" - `a`: 537 MB",
|
|
381
|
+
" - `b`: 1.5 sec",
|
|
382
|
+
]);
|
|
353
383
|
});
|
|
354
384
|
|
|
355
385
|
/*
|
|
356
|
-
* The header used to read "a (By)". Now that
|
|
357
|
-
* landed on, a
|
|
358
|
-
* contradict — and the Components bullet list above the
|
|
359
|
-
* records the configured unit, so nothing is lost.
|
|
386
|
+
* The table header used to read "a (By)". Now that each value says which
|
|
387
|
+
* scale it landed on, a configured unit beside the alias is a claim two
|
|
388
|
+
* samples can contradict — and the Components bullet list above the
|
|
389
|
+
* samples still records the configured unit, so nothing is lost.
|
|
360
390
|
*/
|
|
361
|
-
test("
|
|
391
|
+
test("components are labelled by alias only, not the configured unit", () => {
|
|
362
392
|
const context: string = rootCause(formulaContext());
|
|
363
393
|
|
|
364
|
-
expect(
|
|
365
|
-
|
|
366
|
-
);
|
|
367
|
-
expect(context).not.toContain("| a (By) |");
|
|
394
|
+
expect(context).not.toContain("a (By)");
|
|
395
|
+
expect(context).not.toContain("`a (By)`");
|
|
368
396
|
});
|
|
369
397
|
|
|
370
398
|
test("the Components bullet list spells each component's unit out", () => {
|
|
@@ -376,14 +404,18 @@ describe("MonitorCriteriaEvaluator - Breaching Samples component columns", () =>
|
|
|
376
404
|
);
|
|
377
405
|
});
|
|
378
406
|
|
|
379
|
-
test("a component with no value for this sample
|
|
407
|
+
test("a component with no value for this sample is left out, not printed as a dash", () => {
|
|
380
408
|
const ctx: MetricCriteriaContext = formulaContext();
|
|
381
409
|
ctx.breachingSamples![0]!.componentValues = [
|
|
382
410
|
{ alias: "a", value: 536870912 },
|
|
383
411
|
{ alias: "b", value: null },
|
|
384
412
|
];
|
|
385
413
|
|
|
386
|
-
|
|
414
|
+
const context: string = rootCause(ctx);
|
|
415
|
+
|
|
416
|
+
expect(sampleDetails(context)).toEqual([" - `a`: 537 MB"]);
|
|
417
|
+
expect(context).not.toContain("`b`:");
|
|
418
|
+
expect(context).not.toContain(": -");
|
|
387
419
|
});
|
|
388
420
|
});
|
|
389
421
|
|
|
@@ -438,7 +470,7 @@ describe("MonitorCriteriaEvaluator - formula names are not metric names", () =>
|
|
|
438
470
|
}),
|
|
439
471
|
);
|
|
440
472
|
|
|
441
|
-
expect(
|
|
473
|
+
expect(sampleValue(context)).toBe("0.42");
|
|
442
474
|
expect(context).not.toContain("42.00%");
|
|
443
475
|
expect(context).not.toContain("- Unit:");
|
|
444
476
|
});
|
|
@@ -470,7 +502,7 @@ describe("MonitorCriteriaEvaluator - formula names are not metric names", () =>
|
|
|
470
502
|
}),
|
|
471
503
|
);
|
|
472
504
|
|
|
473
|
-
expect(
|
|
505
|
+
expect(sampleDetails(context)).toEqual([" - `x`: 0.42"]);
|
|
474
506
|
expect(context).not.toContain("42.00%");
|
|
475
507
|
});
|
|
476
508
|
|
|
@@ -490,7 +522,7 @@ describe("MonitorCriteriaEvaluator - formula names are not metric names", () =>
|
|
|
490
522
|
}),
|
|
491
523
|
);
|
|
492
524
|
|
|
493
|
-
expect(
|
|
525
|
+
expect(sampleValue(context)).toBe("42.00%");
|
|
494
526
|
});
|
|
495
527
|
});
|
|
496
528
|
|
|
@@ -500,20 +532,20 @@ describe("MonitorCriteriaEvaluator - formula names are not metric names", () =>
|
|
|
500
532
|
* The unit-suppression rule was hand-rolled three separate times before
|
|
501
533
|
* this change — in CompareCriteria's "Filter Conditions Met" sentence, in
|
|
502
534
|
* MonitorCriteriaObservationBuilder, and nowhere at all in the Breaching
|
|
503
|
-
* Samples table — with the result that a single evaluation
|
|
504
|
-
* differently in its own two halves: "0.31" in the sentence and
|
|
505
|
-
* in the table directly beneath it.
|
|
535
|
+
* Samples table (now a list) — with the result that a single evaluation
|
|
536
|
+
* read differently in its own two halves: "0.31" in the sentence and
|
|
537
|
+
* "0.31 1" in the table directly beneath it.
|
|
506
538
|
*
|
|
507
539
|
* Both sections of one email now go through MetricValueFormatter. These
|
|
508
540
|
* tests assert the two strings are IDENTICAL rather than merely both
|
|
509
541
|
* plausible, so a fourth copy of the rule cannot quietly appear.
|
|
510
542
|
*/
|
|
511
|
-
describe("MonitorCriteriaEvaluator - the
|
|
543
|
+
describe("MonitorCriteriaEvaluator - the Breaching Samples list and the sentence agree", () => {
|
|
512
544
|
function bothRenderings(input: {
|
|
513
545
|
value: number;
|
|
514
546
|
unit: string | null;
|
|
515
547
|
metricName: string;
|
|
516
|
-
}): {
|
|
548
|
+
}): { sampleValue: string; sentence: string } {
|
|
517
549
|
const ctx: MetricCriteriaContext = makeContext({
|
|
518
550
|
metricName: input.metricName,
|
|
519
551
|
unit: input.unit,
|
|
@@ -522,14 +554,8 @@ describe("MonitorCriteriaEvaluator - the table and the sentence agree", () => {
|
|
|
522
554
|
],
|
|
523
555
|
});
|
|
524
556
|
|
|
525
|
-
const row: string = sampleRow(rootCause(ctx));
|
|
526
|
-
const cells: Array<string> = row.split("|").map((cell: string) => {
|
|
527
|
-
return cell.trim();
|
|
528
|
-
});
|
|
529
|
-
|
|
530
557
|
return {
|
|
531
|
-
|
|
532
|
-
cell: cells[4] as string,
|
|
558
|
+
sampleValue: sampleValue(rootCause(ctx)),
|
|
533
559
|
sentence: CompareCriteria.getCompareMessage({
|
|
534
560
|
values: input.value,
|
|
535
561
|
threshold: input.value,
|
|
@@ -559,12 +585,12 @@ describe("MonitorCriteriaEvaluator - the table and the sentence agree", () => {
|
|
|
559
585
|
|
|
560
586
|
for (const testCase of cases) {
|
|
561
587
|
test(`${testCase.metricName} @ ${testCase.unit ?? "no unit"} reads the same in both`, () => {
|
|
562
|
-
const rendered: {
|
|
588
|
+
const rendered: { sampleValue: string; sentence: string } =
|
|
563
589
|
bothRenderings(testCase);
|
|
564
590
|
|
|
565
|
-
expect(rendered.
|
|
591
|
+
expect(rendered.sampleValue.length).toBeGreaterThan(0);
|
|
566
592
|
expect(rendered.sentence).toBe(
|
|
567
|
-
`${testCase.metricName} was ${rendered.
|
|
593
|
+
`${testCase.metricName} was ${rendered.sampleValue}. The condition requires the value to be above the ${rendered.sampleValue} threshold.`,
|
|
568
594
|
);
|
|
569
595
|
});
|
|
570
596
|
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import RootCauseList, {
|
|
2
|
+
RootCauseListItem,
|
|
3
|
+
} from "../../../../Server/Utils/Monitor/RootCauseList";
|
|
4
|
+
import { marked, Tokens, Token } from "marked";
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* RootCauseList is the numbered list both per-item breakdowns of a root
|
|
9
|
+
* cause render as — a platform monitor's Affected Resources and a metric
|
|
10
|
+
* monitor's Breaching Samples. Each has its own suite for its own content
|
|
11
|
+
* (AffectedResourceList.test.ts, MonitorCriteriaEvaluatorBreachingSamples
|
|
12
|
+
* .test.ts); this one pins the shape they share.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function item(overrides: Partial<RootCauseListItem> = {}): RootCauseListItem {
|
|
16
|
+
return {
|
|
17
|
+
title: "`2026-08-14T10:30:00.000Z`",
|
|
18
|
+
value: "**1.07 GB**",
|
|
19
|
+
details: [
|
|
20
|
+
{ label: "`a`", value: "537 MB" },
|
|
21
|
+
{ label: "`k8s.pod.name`", value: "`web-1`" },
|
|
22
|
+
],
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function items(count: number): Array<RootCauseListItem> {
|
|
28
|
+
const result: Array<RootCauseListItem> = [];
|
|
29
|
+
|
|
30
|
+
for (let i: number = 1; i <= count; i++) {
|
|
31
|
+
result.push(
|
|
32
|
+
item({
|
|
33
|
+
title: `\`item-${i}\``,
|
|
34
|
+
value: `**${i}**`,
|
|
35
|
+
details: [{ label: "Label", value: `\`detail-${i}\`` }],
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("RootCauseList.render", () => {
|
|
44
|
+
test("renders each item as a numbered line with its details as nested bullets", () => {
|
|
45
|
+
expect(RootCauseList.render([item()])).toBe(
|
|
46
|
+
[
|
|
47
|
+
"1. `2026-08-14T10:30:00.000Z` — **1.07 GB**",
|
|
48
|
+
" - `a`: 537 MB",
|
|
49
|
+
" - `k8s.pod.name`: `web-1`",
|
|
50
|
+
].join("\n"),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("returns just the list — no heading, and no leading or trailing blank lines", () => {
|
|
55
|
+
const markdown: string = RootCauseList.render(items(2));
|
|
56
|
+
|
|
57
|
+
expect(markdown.startsWith("1. ")).toBe(true);
|
|
58
|
+
expect(markdown.endsWith("`detail-2`")).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns an empty string for no items", () => {
|
|
62
|
+
expect(RootCauseList.render([])).toBe("");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("numbers the items from 1 in the order given", () => {
|
|
66
|
+
const markdown: string = RootCauseList.render(items(3));
|
|
67
|
+
|
|
68
|
+
expect(markdown.split("\n")).toEqual([
|
|
69
|
+
"1. `item-1` — **1**",
|
|
70
|
+
" - Label: `detail-1`",
|
|
71
|
+
"2. `item-2` — **2**",
|
|
72
|
+
" - Label: `detail-2`",
|
|
73
|
+
"3. `item-3` — **3**",
|
|
74
|
+
" - Label: `detail-3`",
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("indents details to the content column: three spaces under 1-9, four under 10-99, five under 100", () => {
|
|
79
|
+
const markdown: string = RootCauseList.render(items(100));
|
|
80
|
+
|
|
81
|
+
expect(markdown).toContain("9. `item-9` — **9**\n - Label: `detail-9`");
|
|
82
|
+
expect(markdown).toContain(
|
|
83
|
+
"10. `item-10` — **10**\n - Label: `detail-10`",
|
|
84
|
+
);
|
|
85
|
+
expect(markdown).toContain(
|
|
86
|
+
"99. `item-99` — **99**\n - Label: `detail-99`",
|
|
87
|
+
);
|
|
88
|
+
expect(markdown).toContain(
|
|
89
|
+
"100. `item-100` — **100**\n - Label: `detail-100`",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a detail with an empty or blank value is left out", () => {
|
|
94
|
+
const markdown: string = RootCauseList.render([
|
|
95
|
+
item({
|
|
96
|
+
details: [
|
|
97
|
+
{ label: "`a`", value: "537 MB" },
|
|
98
|
+
{ label: "`b`", value: "" },
|
|
99
|
+
{ label: "`c`", value: " " },
|
|
100
|
+
{ label: "`d`", value: "1.5 sec" },
|
|
101
|
+
],
|
|
102
|
+
}),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
expect(markdown.split("\n")).toEqual([
|
|
106
|
+
"1. `2026-08-14T10:30:00.000Z` — **1.07 GB**",
|
|
107
|
+
" - `a`: 537 MB",
|
|
108
|
+
" - `d`: 1.5 sec",
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a detail with an empty label is just its value", () => {
|
|
113
|
+
const markdown: string = RootCauseList.render([
|
|
114
|
+
item({ details: [{ label: "", value: "`web-1`" }] }),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
expect(markdown).toBe(
|
|
118
|
+
"1. `2026-08-14T10:30:00.000Z` — **1.07 GB**\n - `web-1`",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("an item without details is a single line", () => {
|
|
123
|
+
expect(RootCauseList.render([item({ details: [] })])).toBe(
|
|
124
|
+
"1. `2026-08-14T10:30:00.000Z` — **1.07 GB**",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("an empty value leaves just the title, with no dangling dash", () => {
|
|
129
|
+
expect(RootCauseList.render([item({ value: "", details: [] })])).toBe(
|
|
130
|
+
"1. `2026-08-14T10:30:00.000Z`",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("an empty title leaves just the value", () => {
|
|
135
|
+
expect(RootCauseList.render([item({ title: " ", details: [] })])).toBe(
|
|
136
|
+
"1. **1.07 GB**",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("trims the whitespace around the title, value, labels and values", () => {
|
|
141
|
+
expect(
|
|
142
|
+
RootCauseList.render([
|
|
143
|
+
item({
|
|
144
|
+
title: " `t` ",
|
|
145
|
+
value: " **v** ",
|
|
146
|
+
details: [{ label: " `k` ", value: " `x` " }],
|
|
147
|
+
}),
|
|
148
|
+
]),
|
|
149
|
+
).toBe("1. `t` — **v**\n - `k`: `x`");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("renders labels, titles and values as markdown, unescaped", () => {
|
|
153
|
+
/*
|
|
154
|
+
* The caller decides: AffectedResourceList escapes its plain-text
|
|
155
|
+
* labels, the Breaching Samples block code-spans its attribute keys.
|
|
156
|
+
*/
|
|
157
|
+
expect(
|
|
158
|
+
RootCauseList.render([
|
|
159
|
+
item({
|
|
160
|
+
title: "**Pod** `x`",
|
|
161
|
+
details: [{ label: "*Namespace*", value: "`y`" }],
|
|
162
|
+
}),
|
|
163
|
+
]),
|
|
164
|
+
).toBe("1. **Pod** `x` — **1.07 GB**\n - *Namespace*: `y`");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("parses as one ordered list whose every item owns a nested bullet list, with no code blocks", () => {
|
|
168
|
+
const tokens: Array<Token> = marked
|
|
169
|
+
.lexer(RootCauseList.render(items(12)))
|
|
170
|
+
.filter((token: Token): boolean => {
|
|
171
|
+
return token.type !== "space";
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(tokens).toHaveLength(1);
|
|
175
|
+
|
|
176
|
+
const list: Tokens.List = tokens[0] as Tokens.List;
|
|
177
|
+
|
|
178
|
+
expect(list.type).toBe("list");
|
|
179
|
+
expect(list.ordered).toBe(true);
|
|
180
|
+
expect(list.items).toHaveLength(12);
|
|
181
|
+
|
|
182
|
+
list.items.forEach((listItem: Tokens.ListItem, index: number) => {
|
|
183
|
+
const nested: Tokens.List | undefined = listItem.tokens.find(
|
|
184
|
+
(token: Token): boolean => {
|
|
185
|
+
return token.type === "list";
|
|
186
|
+
},
|
|
187
|
+
) as Tokens.List | undefined;
|
|
188
|
+
|
|
189
|
+
expect(nested).toBeDefined();
|
|
190
|
+
expect(nested!.ordered).toBe(false);
|
|
191
|
+
expect(
|
|
192
|
+
nested!.items.map((detail: Tokens.ListItem) => {
|
|
193
|
+
return detail.text;
|
|
194
|
+
}),
|
|
195
|
+
).toEqual([`Label: \`detail-${index + 1}\``]);
|
|
196
|
+
|
|
197
|
+
expect(
|
|
198
|
+
listItem.tokens.some((token: Token): boolean => {
|
|
199
|
+
return token.type === "code";
|
|
200
|
+
}),
|
|
201
|
+
).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("RootCauseList.code", () => {
|
|
207
|
+
test("wraps a value in single backticks", () => {
|
|
208
|
+
expect(RootCauseList.code("web-1")).toBe("`web-1`");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("an ISO timestamp stays a bare single-backtick span, which the dashboard localizes", () => {
|
|
212
|
+
expect(RootCauseList.code("2026-08-14T10:30:00.000Z")).toBe(
|
|
213
|
+
"`2026-08-14T10:30:00.000Z`",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test.each([
|
|
218
|
+
["undefined", undefined],
|
|
219
|
+
["null", null],
|
|
220
|
+
["an empty string", ""],
|
|
221
|
+
["whitespace", " \t "],
|
|
222
|
+
["line breaks", "\r\n\n"],
|
|
223
|
+
])("returns an empty string for %s", (_label: string, value: unknown) => {
|
|
224
|
+
expect(RootCauseList.code(value as string | undefined | null)).toBe("");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("turns line breaks into single spaces", () => {
|
|
228
|
+
expect(RootCauseList.code("a\nb\r\nc \n\n d")).toBe("`a b c d`");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("a value with backticks gets a fence one longer than its longest run, padded with spaces", () => {
|
|
232
|
+
expect(RootCauseList.code("a`b")).toBe("`` a`b ``");
|
|
233
|
+
expect(RootCauseList.code("a``b`c")).toBe("``` a``b`c ```");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("the result parses back to exactly the value", () => {
|
|
237
|
+
for (const value of ["a`b", "`edge`", "x``y", "**not bold**", "a|b"]) {
|
|
238
|
+
const tokens: Array<Token> = marked.lexer(
|
|
239
|
+
`before ${RootCauseList.code(value)} after`,
|
|
240
|
+
);
|
|
241
|
+
const paragraph: Tokens.Paragraph = tokens[0] as Tokens.Paragraph;
|
|
242
|
+
const codespan: Tokens.Codespan | undefined = paragraph.tokens.find(
|
|
243
|
+
(token: Token): boolean => {
|
|
244
|
+
return token.type === "codespan";
|
|
245
|
+
},
|
|
246
|
+
) as Tokens.Codespan | undefined;
|
|
247
|
+
|
|
248
|
+
expect(codespan).toBeDefined();
|
|
249
|
+
expect(codespan!.text).toBe(value);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|