@oneuptime/common 11.5.5 → 11.5.7

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 (41) hide show
  1. package/Models/DatabaseModels/User.ts +47 -0
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1784033837629-MigrationName.ts +11 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.ts +23 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Middleware/MasterAdminAuthorization.ts +37 -0
  6. package/Server/Middleware/ProjectAuthorization.ts +40 -15
  7. package/Server/Services/TeamMemberService.ts +2 -0
  8. package/Server/Services/UserService.ts +10 -0
  9. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +16 -6
  10. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +14 -0
  11. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +14 -3
  12. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +130 -12
  13. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +553 -0
  14. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +590 -0
  15. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageFormatterUnits.test.ts +254 -0
  16. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +821 -0
  17. package/build/dist/Models/DatabaseModels/User.js +46 -0
  18. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  19. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784033837629-MigrationName.js +9 -0
  20. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784033837629-MigrationName.js.map +1 -1
  21. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.js +18 -0
  22. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784048917994-AddCreatedByUserToUser.js.map +1 -0
  23. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  24. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  25. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +26 -0
  26. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  27. package/build/dist/Server/Middleware/ProjectAuthorization.js +40 -14
  28. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  29. package/build/dist/Server/Services/TeamMemberService.js +2 -0
  30. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  31. package/build/dist/Server/Services/UserService.js +8 -0
  32. package/build/dist/Server/Services/UserService.js.map +1 -1
  33. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +15 -7
  34. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  35. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +12 -1
  36. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  37. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +8 -3
  38. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  39. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +96 -12
  40. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,553 @@
1
+ import MonitorCriteriaExpectationBuilder from "../../../../Server/Utils/Monitor/MonitorCriteriaExpectationBuilder";
2
+ import {
3
+ CriteriaFilter,
4
+ CheckOn,
5
+ EvaluateOverTimeType,
6
+ FilterType,
7
+ } from "../../../../Types/Monitor/CriteriaFilter";
8
+
9
+ /*
10
+ * Exhaustive unit coverage for the pure function
11
+ * MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, options?).
12
+ *
13
+ * No metric fixtures are needed — every case constructs a plain
14
+ * CriteriaFilter object and asserts the exact rendered expectation string.
15
+ */
16
+
17
+ // Small factory so each test only tweaks the fields it cares about.
18
+ function makeFilter(overrides: Partial<CriteriaFilter>): CriteriaFilter {
19
+ return {
20
+ checkOn: CheckOn.MetricValue,
21
+ filterType: FilterType.GreaterThan,
22
+ value: "5",
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ describe("MonitorCriteriaExpectationBuilder.describeCriteriaExpectation", () => {
28
+ /*
29
+ * Group A — every numeric-comparison filter type appends the unit suffix
30
+ * (" <unit>") directly after the raw threshold value.
31
+ */
32
+ describe("numeric-comparison filter types append the unit", () => {
33
+ test("GreaterThan => 'to be greater than 5 sec'", () => {
34
+ const filter: CriteriaFilter = makeFilter({
35
+ filterType: FilterType.GreaterThan,
36
+ value: "5",
37
+ });
38
+
39
+ const result: string | null =
40
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
41
+ unit: "sec",
42
+ });
43
+
44
+ expect(result).toBe("to be greater than 5 sec");
45
+ });
46
+
47
+ test("GreaterThanOrEqualTo => 'to be greater than or equal to 5 sec'", () => {
48
+ const filter: CriteriaFilter = makeFilter({
49
+ filterType: FilterType.GreaterThanOrEqualTo,
50
+ value: "5",
51
+ });
52
+
53
+ const result: string | null =
54
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
55
+ unit: "sec",
56
+ });
57
+
58
+ expect(result).toBe("to be greater than or equal to 5 sec");
59
+ });
60
+
61
+ test("LessThan => 'to be less than 5 sec'", () => {
62
+ const filter: CriteriaFilter = makeFilter({
63
+ filterType: FilterType.LessThan,
64
+ value: "5",
65
+ });
66
+
67
+ const result: string | null =
68
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
69
+ unit: "sec",
70
+ });
71
+
72
+ expect(result).toBe("to be less than 5 sec");
73
+ });
74
+
75
+ test("LessThanOrEqualTo => 'to be less than or equal to 5 sec'", () => {
76
+ const filter: CriteriaFilter = makeFilter({
77
+ filterType: FilterType.LessThanOrEqualTo,
78
+ value: "5",
79
+ });
80
+
81
+ const result: string | null =
82
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
83
+ unit: "sec",
84
+ });
85
+
86
+ expect(result).toBe("to be less than or equal to 5 sec");
87
+ });
88
+
89
+ test("EqualTo => 'to equal 5 sec'", () => {
90
+ const filter: CriteriaFilter = makeFilter({
91
+ filterType: FilterType.EqualTo,
92
+ value: "5",
93
+ });
94
+
95
+ const result: string | null =
96
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
97
+ unit: "sec",
98
+ });
99
+
100
+ expect(result).toBe("to equal 5 sec");
101
+ });
102
+
103
+ test("NotEqualTo => 'to not equal 5 sec'", () => {
104
+ const filter: CriteriaFilter = makeFilter({
105
+ filterType: FilterType.NotEqualTo,
106
+ value: "5",
107
+ });
108
+
109
+ const result: string | null =
110
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
111
+ unit: "sec",
112
+ });
113
+
114
+ expect(result).toBe("to not equal 5 sec");
115
+ });
116
+ });
117
+
118
+ /*
119
+ * Group B — the threshold value is interpolated RAW via a template string,
120
+ * never number-formatted. "5" stays "5" (not "5.00"), "0.9" stays "0.9",
121
+ * and a numeric 5 also renders as "5".
122
+ */
123
+ describe("threshold value is printed raw (no number formatting)", () => {
124
+ test("string value '5' renders as '5', not '5.00'", () => {
125
+ const filter: CriteriaFilter = makeFilter({
126
+ filterType: FilterType.GreaterThan,
127
+ value: "5",
128
+ });
129
+
130
+ const result: string | null =
131
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
132
+
133
+ expect(result).toBe("to be greater than 5");
134
+ expect(result).not.toContain("5.00");
135
+ });
136
+
137
+ test("string value '0.9' renders as '0.9'", () => {
138
+ const filter: CriteriaFilter = makeFilter({
139
+ filterType: FilterType.GreaterThanOrEqualTo,
140
+ value: "0.9",
141
+ });
142
+
143
+ const result: string | null =
144
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
145
+
146
+ expect(result).toBe("to be greater than or equal to 0.9");
147
+ });
148
+
149
+ test("numeric value 5 renders as '5' (not '5.00')", () => {
150
+ const filter: CriteriaFilter = makeFilter({
151
+ filterType: FilterType.GreaterThan,
152
+ value: 5,
153
+ });
154
+
155
+ const result: string | null =
156
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
157
+
158
+ expect(result).toBe("to be greater than 5");
159
+ expect(result).not.toContain("5.00");
160
+ });
161
+ });
162
+
163
+ /*
164
+ * Group C — non-numeric filter types supply their own wording and must
165
+ * NEVER pick up the unit suffix, even when { unit: "sec" } is passed.
166
+ */
167
+ describe("non-numeric filter types ignore the unit", () => {
168
+ test("Contains => 'to contain foo' (no ' sec')", () => {
169
+ const filter: CriteriaFilter = makeFilter({
170
+ filterType: FilterType.Contains,
171
+ value: "foo",
172
+ });
173
+
174
+ const result: string | null =
175
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
176
+ unit: "sec",
177
+ });
178
+
179
+ expect(result).toBe("to contain foo");
180
+ expect(result).not.toContain("sec");
181
+ });
182
+
183
+ test("NotContains => 'to not contain foo' (no ' sec')", () => {
184
+ const filter: CriteriaFilter = makeFilter({
185
+ filterType: FilterType.NotContains,
186
+ value: "foo",
187
+ });
188
+
189
+ const result: string | null =
190
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
191
+ unit: "sec",
192
+ });
193
+
194
+ expect(result).toBe("to not contain foo");
195
+ expect(result).not.toContain("sec");
196
+ });
197
+
198
+ test("StartsWith => 'to start with foo' (no ' sec')", () => {
199
+ const filter: CriteriaFilter = makeFilter({
200
+ filterType: FilterType.StartsWith,
201
+ value: "foo",
202
+ });
203
+
204
+ const result: string | null =
205
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
206
+ unit: "sec",
207
+ });
208
+
209
+ expect(result).toBe("to start with foo");
210
+ expect(result).not.toContain("sec");
211
+ });
212
+
213
+ test("EndsWith => 'to end with foo' (no ' sec')", () => {
214
+ const filter: CriteriaFilter = makeFilter({
215
+ filterType: FilterType.EndsWith,
216
+ value: "foo",
217
+ });
218
+
219
+ const result: string | null =
220
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
221
+ unit: "sec",
222
+ });
223
+
224
+ expect(result).toBe("to end with foo");
225
+ expect(result).not.toContain("sec");
226
+ });
227
+
228
+ test("IsEmpty => 'to be empty' (no unit suffix)", () => {
229
+ const filter: CriteriaFilter = makeFilter({
230
+ filterType: FilterType.IsEmpty,
231
+ value: undefined,
232
+ });
233
+
234
+ const result: string | null =
235
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
236
+ unit: "sec",
237
+ });
238
+
239
+ expect(result).toBe("to be empty");
240
+ expect(result).not.toContain("sec");
241
+ });
242
+
243
+ test("IsNotEmpty => 'to not be empty' (no unit suffix)", () => {
244
+ const filter: CriteriaFilter = makeFilter({
245
+ filterType: FilterType.IsNotEmpty,
246
+ value: undefined,
247
+ });
248
+
249
+ const result: string | null =
250
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
251
+ unit: "sec",
252
+ });
253
+
254
+ expect(result).toBe("to not be empty");
255
+ expect(result).not.toContain("sec");
256
+ });
257
+
258
+ test("True => 'to be true' (no unit suffix)", () => {
259
+ const filter: CriteriaFilter = makeFilter({
260
+ filterType: FilterType.True,
261
+ value: undefined,
262
+ });
263
+
264
+ const result: string | null =
265
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
266
+ unit: "sec",
267
+ });
268
+
269
+ expect(result).toBe("to be true");
270
+ expect(result).not.toContain("sec");
271
+ });
272
+
273
+ test("False => 'to be false' (no unit suffix)", () => {
274
+ const filter: CriteriaFilter = makeFilter({
275
+ filterType: FilterType.False,
276
+ value: undefined,
277
+ });
278
+
279
+ const result: string | null =
280
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
281
+ unit: "sec",
282
+ });
283
+
284
+ expect(result).toBe("to be false");
285
+ expect(result).not.toContain("sec");
286
+ });
287
+
288
+ test("IsExecuting / IsNotExecuting / EvaluatesToTrue get no unit suffix", () => {
289
+ const isExecuting: string | null =
290
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(
291
+ makeFilter({ filterType: FilterType.IsExecuting, value: undefined }),
292
+ { unit: "sec" },
293
+ );
294
+ const isNotExecuting: string | null =
295
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(
296
+ makeFilter({
297
+ filterType: FilterType.IsNotExecuting,
298
+ value: undefined,
299
+ }),
300
+ { unit: "sec" },
301
+ );
302
+ const evaluatesToTrue: string | null =
303
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(
304
+ makeFilter({
305
+ filterType: FilterType.EvaluatesToTrue,
306
+ value: undefined,
307
+ }),
308
+ { unit: "sec" },
309
+ );
310
+
311
+ expect(isExecuting).toBe("to be executing");
312
+ expect(isNotExecuting).toBe("to not be executing");
313
+ expect(evaluatesToTrue).toBe("to evaluate to true");
314
+ expect(isExecuting).not.toContain("sec");
315
+ expect(isNotExecuting).not.toContain("sec");
316
+ expect(evaluatesToTrue).not.toContain("sec");
317
+ });
318
+ });
319
+
320
+ /*
321
+ * Group D — backward compatibility. Calling with no options, { unit: undefined },
322
+ * or { unit: "" } must all produce the exact same unitless string as before.
323
+ */
324
+ describe("backward compatibility (no/empty/undefined unit)", () => {
325
+ test("no second argument => unitless string", () => {
326
+ const filter: CriteriaFilter = makeFilter({
327
+ filterType: FilterType.GreaterThan,
328
+ value: "5",
329
+ });
330
+
331
+ const result: string | null =
332
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
333
+
334
+ expect(result).toBe("to be greater than 5");
335
+ });
336
+
337
+ test("{ unit: undefined } => unitless string", () => {
338
+ const filter: CriteriaFilter = makeFilter({
339
+ filterType: FilterType.GreaterThan,
340
+ value: "5",
341
+ });
342
+
343
+ const result: string | null =
344
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
345
+ unit: undefined,
346
+ });
347
+
348
+ expect(result).toBe("to be greater than 5");
349
+ });
350
+
351
+ test("{ unit: '' } => unitless string", () => {
352
+ const filter: CriteriaFilter = makeFilter({
353
+ filterType: FilterType.GreaterThan,
354
+ value: "5",
355
+ });
356
+
357
+ const result: string | null =
358
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
359
+ unit: "",
360
+ });
361
+
362
+ expect(result).toBe("to be greater than 5");
363
+ });
364
+
365
+ test("all three call forms produce the identical unitless string", () => {
366
+ const filter: CriteriaFilter = makeFilter({
367
+ filterType: FilterType.GreaterThan,
368
+ value: "5",
369
+ });
370
+
371
+ const noArg: string | null =
372
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
373
+ const undefUnit: string | null =
374
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
375
+ unit: undefined,
376
+ });
377
+ const emptyUnit: string | null =
378
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
379
+ unit: "",
380
+ });
381
+
382
+ expect(noArg).toBe("to be greater than 5");
383
+ expect(undefUnit).toBe(noArg);
384
+ expect(emptyUnit).toBe(noArg);
385
+ });
386
+ });
387
+
388
+ /*
389
+ * Group E — evaluation-window clause. The unit is appended to the threshold
390
+ * BEFORE the "over the last N minutes using <agg>" window clause. Aggregation
391
+ * may come from evaluateOverTimeOptions.evaluateOverTimeType OR from
392
+ * metricMonitorOptions.metricAggregationType.
393
+ */
394
+ describe("evaluation window ordering and aggregation", () => {
395
+ test("unit precedes the window clause: 'to be greater than 5 sec over the last 5 minutes using average'", () => {
396
+ const filter: CriteriaFilter = makeFilter({
397
+ filterType: FilterType.GreaterThan,
398
+ value: "5",
399
+ evaluateOverTime: true,
400
+ evaluateOverTimeOptions: {
401
+ timeValueInMinutes: 5,
402
+ evaluateOverTimeType: EvaluateOverTimeType.Average,
403
+ },
404
+ });
405
+
406
+ const result: string | null =
407
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
408
+ unit: "sec",
409
+ });
410
+
411
+ expect(result).toBe(
412
+ "to be greater than 5 sec over the last 5 minutes using average",
413
+ );
414
+ });
415
+
416
+ test("aggregation from metricMonitorOptions.metricAggregationType yields 'using <lowercased>'", () => {
417
+ const filter: CriteriaFilter = makeFilter({
418
+ filterType: FilterType.GreaterThan,
419
+ value: "5",
420
+ metricMonitorOptions: {
421
+ metricAggregationType: EvaluateOverTimeType.Average,
422
+ },
423
+ });
424
+
425
+ const result: string | null =
426
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
427
+
428
+ expect(result).toBe("to be greater than 5 using average");
429
+ });
430
+
431
+ test("aggregation from metricMonitorOptions combines with the unit suffix", () => {
432
+ const filter: CriteriaFilter = makeFilter({
433
+ filterType: FilterType.GreaterThan,
434
+ value: "5",
435
+ metricMonitorOptions: {
436
+ metricAggregationType: EvaluateOverTimeType.MaximumValue,
437
+ },
438
+ });
439
+
440
+ const result: string | null =
441
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
442
+ unit: "sec",
443
+ });
444
+
445
+ // "Maximum Value" lowercased.
446
+ expect(result).toBe("to be greater than 5 sec using maximum value");
447
+ });
448
+ });
449
+
450
+ /*
451
+ * Group F — edge cases: an undefined filterType always returns null (even
452
+ * with a unit), and heartbeat-style filters keep their own wording without a
453
+ * unit suffix.
454
+ */
455
+ describe("edge cases", () => {
456
+ test("filterType undefined => null (no unit)", () => {
457
+ const filter: CriteriaFilter = makeFilter({
458
+ filterType: undefined,
459
+ value: "5",
460
+ });
461
+
462
+ const result: string | null =
463
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter);
464
+
465
+ expect(result).toBeNull();
466
+ });
467
+
468
+ test("filterType undefined => null (even when a unit is passed)", () => {
469
+ const filter: CriteriaFilter = makeFilter({
470
+ filterType: undefined,
471
+ value: "5",
472
+ });
473
+
474
+ const result: string | null =
475
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
476
+ unit: "sec",
477
+ });
478
+
479
+ expect(result).toBeNull();
480
+ });
481
+
482
+ test("RecievedInMinutes with a value keeps its heartbeat wording and gets no unit suffix", () => {
483
+ const filter: CriteriaFilter = makeFilter({
484
+ checkOn: CheckOn.IncomingRequest,
485
+ filterType: FilterType.RecievedInMinutes,
486
+ value: "5",
487
+ });
488
+
489
+ const result: string | null =
490
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
491
+ unit: "sec",
492
+ });
493
+
494
+ expect(result).toBe("to receive a heartbeat within 5 minutes");
495
+ expect(result).not.toContain("sec");
496
+ });
497
+
498
+ test("NotRecievedInMinutes with a value keeps its heartbeat wording and gets no unit suffix", () => {
499
+ const filter: CriteriaFilter = makeFilter({
500
+ checkOn: CheckOn.IncomingRequest,
501
+ filterType: FilterType.NotRecievedInMinutes,
502
+ value: "5",
503
+ });
504
+
505
+ const result: string | null =
506
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
507
+ unit: "sec",
508
+ });
509
+
510
+ expect(result).toBe("to miss a heartbeat for at least 5 minutes");
511
+ expect(result).not.toContain("sec");
512
+ });
513
+ });
514
+
515
+ /*
516
+ * Group G — negative thresholds. The value is interpolated RAW via the
517
+ * template string, so a negative sign and any decimals survive verbatim
518
+ * (no toFixed / number formatting), and the unit suffix still follows.
519
+ */
520
+ describe("negative thresholds with a unit are interpolated raw", () => {
521
+ test("GreaterThan '-5' with { unit: 'sec' } => 'to be greater than -5 sec'", () => {
522
+ const filter: CriteriaFilter = makeFilter({
523
+ filterType: FilterType.GreaterThan,
524
+ value: "-5",
525
+ });
526
+
527
+ const result: string | null =
528
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
529
+ unit: "sec",
530
+ });
531
+
532
+ expect(result).toBe("to be greater than -5 sec");
533
+ // Raw interpolation — never number-formatted.
534
+ expect(result).not.toContain("-5.00");
535
+ });
536
+
537
+ test("LessThan '-0.5' with { unit: 'sec' } => 'to be less than -0.5 sec'", () => {
538
+ const filter: CriteriaFilter = makeFilter({
539
+ filterType: FilterType.LessThan,
540
+ value: "-0.5",
541
+ });
542
+
543
+ const result: string | null =
544
+ MonitorCriteriaExpectationBuilder.describeCriteriaExpectation(filter, {
545
+ unit: "sec",
546
+ });
547
+
548
+ expect(result).toBe("to be less than -0.5 sec");
549
+ // Raw interpolation — the decimal is kept as-is, not padded to "-0.50".
550
+ expect(result).not.toContain("-0.50");
551
+ });
552
+ });
553
+ });