@oneuptime/common 13.0.3 → 13.0.4
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/Tests/Server/Utils/LoggerCore.test.ts +597 -0
- package/Tests/Server/Utils/Monitor/MonitorTemplateUtil.test.ts +1109 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +711 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +522 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilCorePaths.test.ts +1014 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +9 -6
- package/Tests/Types/Rules/RuleCriteriaFieldRegistry.test.ts +276 -0
- package/Tests/Types/Telemetry/LlmConventions.test.ts +495 -0
- package/Tests/Utils/Rum/SessionReplayHealth.test.ts +884 -0
- package/Types/OnCallDutyPolicy/Layer.ts +12 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +11 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +9 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/package.json +1 -1
|
@@ -761,7 +761,7 @@ describe("Edge cases", () => {
|
|
|
761
761
|
}
|
|
762
762
|
});
|
|
763
763
|
|
|
764
|
-
it("
|
|
764
|
+
it("getEvents does not emit a trailing negative-length event when the window end lands exactly on a rotation boundary", () => {
|
|
765
765
|
const cfg: Cfg = {
|
|
766
766
|
name: "boundary",
|
|
767
767
|
intervalType: EventInterval.Day,
|
|
@@ -781,12 +781,15 @@ describe("Edge cases", () => {
|
|
|
781
781
|
});
|
|
782
782
|
const last: CalendarEvent = raw[raw.length - 1]!;
|
|
783
783
|
/*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
* user-observable defect, so it is documented rather than reported.
|
|
784
|
+
* The final event used to be inverted (end 1s before start). getEvents is
|
|
785
|
+
* consumed directly (layer shift preview, calendar feed coverage
|
|
786
|
+
* envelope), so it must never emit one.
|
|
788
787
|
*/
|
|
789
|
-
expect(OneUptimeDate.
|
|
788
|
+
expect(OneUptimeDate.isAfter(last.end, last.start)).toBe(true);
|
|
789
|
+
expect(OneUptimeDate.isSame(last.end, calEnd)).toBe(true);
|
|
790
|
+
for (const e of raw) {
|
|
791
|
+
expect(OneUptimeDate.isAfter(e.end, e.start)).toBe(true);
|
|
792
|
+
}
|
|
790
793
|
|
|
791
794
|
const multi: CalendarEvent[] = util.getMultiLayerEvents({
|
|
792
795
|
layers: [layerOf(cfg)],
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { describe, expect, test } from "@jest/globals";
|
|
4
|
+
import RULE_CRITERIA_FIELDS_BY_MODEL, {
|
|
5
|
+
RULE_CRITERIA_FIELDS_BY_MODEL as NAMED_RULE_CRITERIA_FIELDS_BY_MODEL,
|
|
6
|
+
getRuleCriteriaFieldsForModel,
|
|
7
|
+
} from "../../../Types/Rules/RuleCriteriaFieldRegistry";
|
|
8
|
+
|
|
9
|
+
const DATABASE_MODELS_ROOT: string = path.join(
|
|
10
|
+
__dirname,
|
|
11
|
+
"../../../Models/DatabaseModels",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const MODEL_NAMES: Array<string> = Object.keys(RULE_CRITERIA_FIELDS_BY_MODEL);
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* Fields that describe what a rule DOES rather than what it matches. None of
|
|
18
|
+
* them may ever become an allowlisted match predicate: a filter on
|
|
19
|
+
* "ownerUsers" would let an API caller make a rule match on its own action.
|
|
20
|
+
*/
|
|
21
|
+
const ACTION_OR_BOOKKEEPING_FIELDS: Array<string> = [
|
|
22
|
+
"_id",
|
|
23
|
+
"id",
|
|
24
|
+
"projectId",
|
|
25
|
+
"project",
|
|
26
|
+
"name",
|
|
27
|
+
"description",
|
|
28
|
+
"isEnabled",
|
|
29
|
+
"priority",
|
|
30
|
+
"order",
|
|
31
|
+
"createdAt",
|
|
32
|
+
"updatedAt",
|
|
33
|
+
"deletedAt",
|
|
34
|
+
"createdByUser",
|
|
35
|
+
"createdByUserId",
|
|
36
|
+
"deletedByUser",
|
|
37
|
+
"deletedByUserId",
|
|
38
|
+
"ownerUsers",
|
|
39
|
+
"ownerTeams",
|
|
40
|
+
"labelsToAdd",
|
|
41
|
+
"onCallDutyPolicies",
|
|
42
|
+
"criteria",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function readModelSource(modelName: string): string {
|
|
46
|
+
return fs.readFileSync(
|
|
47
|
+
path.join(DATABASE_MODELS_ROOT, `${modelName}.ts`),
|
|
48
|
+
"utf8",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("RULE_CRITERIA_FIELDS_BY_MODEL", () => {
|
|
53
|
+
test("the default export and the named export are the same object", () => {
|
|
54
|
+
expect(RULE_CRITERIA_FIELDS_BY_MODEL).toBe(
|
|
55
|
+
NAMED_RULE_CRITERIA_FIELDS_BY_MODEL,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("registers a non-trivial number of rule models", () => {
|
|
60
|
+
expect(MODEL_NAMES.length).toBeGreaterThanOrEqual(70);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("every registered name is a *Rule model name", () => {
|
|
64
|
+
for (const modelName of MODEL_NAMES) {
|
|
65
|
+
expect(modelName).toMatch(/^[A-Z][A-Za-z0-9]*Rule$/);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test.each(MODEL_NAMES)(
|
|
70
|
+
"%s has at least one criteria field",
|
|
71
|
+
(modelName: string) => {
|
|
72
|
+
const fields: ReadonlyArray<string> | undefined =
|
|
73
|
+
RULE_CRITERIA_FIELDS_BY_MODEL[modelName];
|
|
74
|
+
|
|
75
|
+
expect(Array.isArray(fields)).toBe(true);
|
|
76
|
+
expect(fields!.length).toBeGreaterThan(0);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
test.each(MODEL_NAMES)(
|
|
81
|
+
"%s lists each field exactly once",
|
|
82
|
+
(modelName: string) => {
|
|
83
|
+
const fields: ReadonlyArray<string> =
|
|
84
|
+
RULE_CRITERIA_FIELDS_BY_MODEL[modelName]!;
|
|
85
|
+
|
|
86
|
+
expect(new Set(fields).size).toBe(fields.length);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
test.each(MODEL_NAMES)(
|
|
91
|
+
"%s only lists camelCase identifiers",
|
|
92
|
+
(modelName: string) => {
|
|
93
|
+
for (const field of RULE_CRITERIA_FIELDS_BY_MODEL[modelName]!) {
|
|
94
|
+
expect(field).toMatch(/^[a-z][A-Za-z0-9]*$/);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
test.each(MODEL_NAMES)(
|
|
100
|
+
"%s never allowlists an action or bookkeeping field as a match predicate",
|
|
101
|
+
(modelName: string) => {
|
|
102
|
+
for (const field of RULE_CRITERIA_FIELDS_BY_MODEL[modelName]!) {
|
|
103
|
+
expect(ACTION_OR_BOOKKEEPING_FIELDS).not.toContain(field);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
test.each(MODEL_NAMES)(
|
|
109
|
+
"%s names a database model that declares every registered field",
|
|
110
|
+
(modelName: string) => {
|
|
111
|
+
const modelPath: string = path.join(
|
|
112
|
+
DATABASE_MODELS_ROOT,
|
|
113
|
+
`${modelName}.ts`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(fs.existsSync(modelPath)).toBe(true);
|
|
117
|
+
|
|
118
|
+
const source: string = readModelSource(modelName);
|
|
119
|
+
|
|
120
|
+
for (const field of RULE_CRITERIA_FIELDS_BY_MODEL[modelName]!) {
|
|
121
|
+
const declaration: RegExp = new RegExp(
|
|
122
|
+
`public\\s+${field}\\??\\s*[:=]`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect({
|
|
126
|
+
modelName,
|
|
127
|
+
field,
|
|
128
|
+
declared: declaration.test(source),
|
|
129
|
+
}).toEqual({ modelName, field, declared: true });
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
test("Label/Owner/OnCall/Privacy sibling rules for the same entity share one criteria contract", () => {
|
|
135
|
+
const siblingSuffixes: Array<string> = [
|
|
136
|
+
"LabelRule",
|
|
137
|
+
"OwnerRule",
|
|
138
|
+
"OnCallRule",
|
|
139
|
+
"PrivacyRule",
|
|
140
|
+
"GroupingRule",
|
|
141
|
+
];
|
|
142
|
+
const groups: Map<string, Array<string>> = new Map<string, Array<string>>();
|
|
143
|
+
|
|
144
|
+
for (const modelName of MODEL_NAMES) {
|
|
145
|
+
const suffix: string | undefined = siblingSuffixes.find(
|
|
146
|
+
(candidate: string) => {
|
|
147
|
+
return modelName.endsWith(candidate);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!suffix) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entity: string = modelName.slice(0, -suffix.length);
|
|
156
|
+
const members: Array<string> = groups.get(entity) || [];
|
|
157
|
+
members.push(modelName);
|
|
158
|
+
groups.set(entity, members);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let comparedGroups: number = 0;
|
|
162
|
+
|
|
163
|
+
for (const [, members] of groups) {
|
|
164
|
+
if (members.length < 2) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
comparedGroups++;
|
|
169
|
+
const expected: ReadonlyArray<string> =
|
|
170
|
+
RULE_CRITERIA_FIELDS_BY_MODEL[members[0]!]!;
|
|
171
|
+
|
|
172
|
+
for (const member of members.slice(1)) {
|
|
173
|
+
expect({
|
|
174
|
+
member,
|
|
175
|
+
fields: RULE_CRITERIA_FIELDS_BY_MODEL[member],
|
|
176
|
+
}).toEqual({ member, fields: expected });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
expect(comparedGroups).toBeGreaterThan(20);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("alert rules match on alert severities and incident rules on incident severities, never the other way round", () => {
|
|
184
|
+
for (const modelName of MODEL_NAMES) {
|
|
185
|
+
const fields: ReadonlyArray<string> =
|
|
186
|
+
RULE_CRITERIA_FIELDS_BY_MODEL[modelName]!;
|
|
187
|
+
|
|
188
|
+
if (modelName.startsWith("Alert")) {
|
|
189
|
+
expect(fields).not.toContain("incidentSeverities");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (modelName.startsWith("Incident")) {
|
|
193
|
+
expect(fields).not.toContain("alertSeverities");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("AutoRemediationRule is the one rule that can match both incident and alert severities", () => {
|
|
199
|
+
const both: Array<string> = MODEL_NAMES.filter((modelName: string) => {
|
|
200
|
+
const fields: ReadonlyArray<string> =
|
|
201
|
+
RULE_CRITERIA_FIELDS_BY_MODEL[modelName]!;
|
|
202
|
+
return (
|
|
203
|
+
fields.includes("incidentSeverities") &&
|
|
204
|
+
fields.includes("alertSeverities")
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(both).toEqual(["AutoRemediationRule"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("pins a few representative contracts exactly", () => {
|
|
212
|
+
expect(RULE_CRITERIA_FIELDS_BY_MODEL["AlertReminderRule"]).toEqual([
|
|
213
|
+
"alertSeverities",
|
|
214
|
+
"labels",
|
|
215
|
+
]);
|
|
216
|
+
expect(
|
|
217
|
+
RULE_CRITERIA_FIELDS_BY_MODEL["ScheduledMaintenanceReminderRule"],
|
|
218
|
+
).toEqual(["labels"]);
|
|
219
|
+
expect(RULE_CRITERIA_FIELDS_BY_MODEL["NetworkSiteAssignmentRule"]).toEqual([
|
|
220
|
+
"subnetCidr",
|
|
221
|
+
"hostnamePattern",
|
|
222
|
+
]);
|
|
223
|
+
expect(RULE_CRITERIA_FIELDS_BY_MODEL["RunbookRule"]).toEqual([
|
|
224
|
+
"titlePattern",
|
|
225
|
+
"descriptionPattern",
|
|
226
|
+
]);
|
|
227
|
+
// IncidentSlaRule deliberately carries no monitor name/description patterns.
|
|
228
|
+
expect(RULE_CRITERIA_FIELDS_BY_MODEL["IncidentSlaRule"]).not.toContain(
|
|
229
|
+
"monitorNamePattern",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("getRuleCriteriaFieldsForModel", () => {
|
|
235
|
+
test.each(MODEL_NAMES)(
|
|
236
|
+
"returns the registered fields for %s",
|
|
237
|
+
(modelName: string) => {
|
|
238
|
+
expect(getRuleCriteriaFieldsForModel(modelName)).toBe(
|
|
239
|
+
RULE_CRITERIA_FIELDS_BY_MODEL[modelName],
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
test("returns undefined for a model that is not registered", () => {
|
|
245
|
+
expect(getRuleCriteriaFieldsForModel("Monitor")).toBeUndefined();
|
|
246
|
+
expect(getRuleCriteriaFieldsForModel("NotARealRule")).toBeUndefined();
|
|
247
|
+
expect(getRuleCriteriaFieldsForModel("")).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("is case-sensitive: a near-miss spelling is not registered", () => {
|
|
251
|
+
expect(getRuleCriteriaFieldsForModel("alertlabelrule")).toBeUndefined();
|
|
252
|
+
expect(getRuleCriteriaFieldsForModel("ALERTLABELRULE")).toBeUndefined();
|
|
253
|
+
expect(getRuleCriteriaFieldsForModel(" AlertLabelRule")).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* Regression: the lookup used a plain index, so inherited Object.prototype
|
|
258
|
+
* members answered with a function or object instead of undefined. A caller
|
|
259
|
+
* that treats "not undefined" as "registered" would then call .includes()
|
|
260
|
+
* on a function and throw a TypeError instead of rejecting the model.
|
|
261
|
+
*/
|
|
262
|
+
test.each([
|
|
263
|
+
"constructor",
|
|
264
|
+
"toString",
|
|
265
|
+
"valueOf",
|
|
266
|
+
"hasOwnProperty",
|
|
267
|
+
"isPrototypeOf",
|
|
268
|
+
"__proto__",
|
|
269
|
+
"__defineGetter__",
|
|
270
|
+
])(
|
|
271
|
+
"returns undefined for the inherited property name %s",
|
|
272
|
+
(modelName: string) => {
|
|
273
|
+
expect(getRuleCriteriaFieldsForModel(modelName)).toBeUndefined();
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
});
|