@checkstack/healthcheck-backend 1.15.0 → 1.17.0

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.
@@ -1,8 +1,12 @@
1
+ import { evaluateAssertion } from "@checkstack/backend-api";
2
+ import type {
3
+ AssertionOutcome,
4
+ CollectorAssertion,
5
+ } from "@checkstack/healthcheck-common";
1
6
  import {
2
- evaluateAssertions,
3
- evaluateJsonPathAssertions,
4
- } from "@checkstack/backend-api";
5
- import type { CollectorAssertion } from "@checkstack/healthcheck-common";
7
+ computeAssertionKey,
8
+ truncateActual,
9
+ } from "@checkstack/healthcheck-common";
6
10
  import { extractErrorMessage } from "@checkstack/common";
7
11
  import { JSONPath } from "jsonpath-plus";
8
12
 
@@ -69,9 +73,23 @@ function formatFailure({
69
73
  return detail ? `${base} (${detail})` : base;
70
74
  }
71
75
 
76
+ /** All outcomes of a collector's assertions plus the legacy failure string. */
77
+ export interface CollectorAssertionEvaluation {
78
+ /** One structured outcome per configured assertion, in config order. */
79
+ outcomes: AssertionOutcome[];
80
+ /**
81
+ * The FIRST failing assertion's message, formatted exactly like the legacy
82
+ * `_assertionFailed` string. Undefined when everything passed.
83
+ */
84
+ firstFailureMessage?: string;
85
+ }
86
+
72
87
  /**
73
- * Evaluate a collector's assertions - plain field assertions AND JSONPath
74
- * assertions - against its result, in the order they were configured.
88
+ * Evaluate ALL of a collector's assertions - plain field assertions AND
89
+ * JSONPath assertions - against its result, in the order they were
90
+ * configured, returning a structured outcome per assertion (pass AND fail;
91
+ * this is what makes assertions analyzable rather than only visible on
92
+ * failure).
75
93
  *
76
94
  * Plain assertions compare `result[field]` directly (unchanged behaviour).
77
95
  * JSONPath assertions parse the SOURCE field (e.g. `body` for the field
@@ -79,18 +97,15 @@ function formatFailure({
79
97
  * apply the operator to the extracted value. Fail-closed: a missing
80
98
  * expression, a non-JSON source value, or an invalid/eval-blocked path fails
81
99
  * the assertion (with a diagnostic suffix) - it never fails the collector.
82
- *
83
- * Returns the failure message of the FIRST failing assertion, or `undefined`
84
- * when all pass.
85
100
  */
86
- export function evaluateCollectorAssertions({
101
+ export function evaluateCollectorAssertionOutcomes({
87
102
  assertions,
88
103
  result,
89
104
  }: {
90
105
  assertions: CollectorAssertion[] | undefined;
91
106
  result: Record<string, unknown>;
92
- }): string | undefined {
93
- if (!assertions?.length) return undefined;
107
+ }): CollectorAssertionEvaluation {
108
+ if (!assertions?.length) return { outcomes: [] };
94
109
 
95
110
  // Parse each JSON source field at most once, not once per assertion.
96
111
  const parsedSources = new Map<string, { json?: unknown; error?: string }>();
@@ -116,19 +131,72 @@ export function evaluateCollectorAssertions({
116
131
  return entry;
117
132
  };
118
133
 
134
+ const outcomes: AssertionOutcome[] = [];
135
+ let firstFailureMessage: string | undefined;
136
+
137
+ const baseOutcome = (assertion: CollectorAssertion) => ({
138
+ key: computeAssertionKey({ assertion }),
139
+ field: assertion.field,
140
+ jsonPath: assertion.jsonPath?.trim() || undefined,
141
+ operator: assertion.operator,
142
+ value:
143
+ assertion.value === undefined ? undefined : String(assertion.value),
144
+ });
145
+
146
+ const recordFailure = ({
147
+ assertion,
148
+ actual,
149
+ message,
150
+ legacyMessage,
151
+ }: {
152
+ assertion: CollectorAssertion;
153
+ actual?: unknown;
154
+ message: string;
155
+ legacyMessage: string;
156
+ }) => {
157
+ outcomes.push({
158
+ ...baseOutcome(assertion),
159
+ passed: false,
160
+ actual: actual === undefined ? undefined : truncateActual({ value: actual }),
161
+ message,
162
+ });
163
+ if (firstFailureMessage === undefined) firstFailureMessage = legacyMessage;
164
+ };
165
+
119
166
  for (const assertion of assertions) {
120
167
  if (!isJsonPathAssertion(assertion)) {
121
- const failed = evaluateAssertions([assertion], result);
122
- if (failed) return formatFailure({ assertion: failed });
168
+ const evaluated = evaluateAssertion(assertion, result);
169
+ if (evaluated.passed) {
170
+ outcomes.push({
171
+ ...baseOutcome(assertion),
172
+ passed: true,
173
+ actual:
174
+ evaluated.actual === undefined
175
+ ? undefined
176
+ : truncateActual({ value: evaluated.actual }),
177
+ });
178
+ } else {
179
+ recordFailure({
180
+ assertion,
181
+ actual: evaluated.actual,
182
+ message: evaluated.message ?? formatFailure({ assertion }),
183
+ legacyMessage: formatFailure({ assertion }),
184
+ });
185
+ }
123
186
  continue;
124
187
  }
125
188
 
126
189
  const path = assertion.jsonPath?.trim();
127
190
  if (!path) {
128
- return formatFailure({
191
+ recordFailure({
129
192
  assertion,
130
- detail: "missing JSONPath expression",
193
+ message: "missing JSONPath expression",
194
+ legacyMessage: formatFailure({
195
+ assertion,
196
+ detail: "missing JSONPath expression",
197
+ }),
131
198
  });
199
+ continue;
132
200
  }
133
201
 
134
202
  const sourceField = assertion.field.endsWith(JSONPATH_FIELD_SUFFIX)
@@ -136,34 +204,73 @@ export function evaluateCollectorAssertions({
136
204
  : assertion.field;
137
205
  const source = parseSource(sourceField);
138
206
  if (source.error) {
139
- return formatFailure({ assertion, detail: source.error });
207
+ recordFailure({
208
+ assertion,
209
+ message: source.error,
210
+ legacyMessage: formatFailure({ assertion, detail: source.error }),
211
+ });
212
+ continue;
140
213
  }
141
214
 
142
215
  try {
143
- const failed = evaluateJsonPathAssertions(
144
- [
145
- {
146
- path,
147
- operator: assertion.operator,
148
- value:
149
- assertion.value === undefined
150
- ? undefined
151
- : String(assertion.value),
152
- },
153
- ],
154
- source.json,
155
- extractJsonPath,
216
+ // Extract once, then reuse the shared operator engine on a synthetic
217
+ // one-field record so the outcome carries the observed value.
218
+ const extracted = extractJsonPath(path, source.json);
219
+ const evaluated = evaluateAssertion(
220
+ {
221
+ field: "__jsonpath__",
222
+ operator: assertion.operator,
223
+ value:
224
+ assertion.value === undefined
225
+ ? undefined
226
+ : String(assertion.value),
227
+ },
228
+ { __jsonpath__: extracted },
156
229
  );
157
- if (failed) return formatFailure({ assertion });
230
+ if (evaluated.passed) {
231
+ outcomes.push({
232
+ ...baseOutcome(assertion),
233
+ passed: true,
234
+ actual:
235
+ extracted === undefined
236
+ ? undefined
237
+ : truncateActual({ value: extracted }),
238
+ });
239
+ } else {
240
+ recordFailure({
241
+ assertion,
242
+ actual: extracted,
243
+ message: evaluated.message ?? formatFailure({ assertion }),
244
+ legacyMessage: formatFailure({ assertion }),
245
+ });
246
+ }
158
247
  } catch (error) {
159
248
  // jsonpath-plus rejects malformed paths and (with eval disabled)
160
249
  // filter/script expressions by throwing.
161
- return formatFailure({
250
+ const detail = `invalid JSONPath: ${extractErrorMessage(error)}`;
251
+ recordFailure({
162
252
  assertion,
163
- detail: `invalid JSONPath: ${extractErrorMessage(error)}`,
253
+ message: detail,
254
+ legacyMessage: formatFailure({ assertion, detail }),
164
255
  });
165
256
  }
166
257
  }
167
258
 
168
- return undefined;
259
+ return { outcomes, firstFailureMessage };
260
+ }
261
+
262
+ /**
263
+ * Legacy single-string view of {@link evaluateCollectorAssertionOutcomes}:
264
+ * the failure message of the FIRST failing assertion, or `undefined` when all
265
+ * pass. Kept for callers that only need the `_assertionFailed` string.
266
+ */
267
+ export function evaluateCollectorAssertions({
268
+ assertions,
269
+ result,
270
+ }: {
271
+ assertions: CollectorAssertion[] | undefined;
272
+ result: Record<string, unknown>;
273
+ }): string | undefined {
274
+ return evaluateCollectorAssertionOutcomes({ assertions, result })
275
+ .firstFailureMessage;
169
276
  }
package/src/index.ts CHANGED
@@ -524,6 +524,35 @@ export default createBackendPlugin({
524
524
  logger,
525
525
  signalService,
526
526
  configSecrets,
527
+ // Fold active incident health overrides into the user-facing system
528
+ // health reads (worst-wins). Reuses the incident client already built
529
+ // above; maps the incident rows to the source-agnostic override shape
530
+ // the fold expects. Kept out of the shared deriver so SLO/AI/entity
531
+ // paths stay checks-only (see router.ts).
532
+ incidentHealthOverrideReader: {
533
+ getActiveOverrides: async (systemIds) => {
534
+ const { overrides } =
535
+ await incidentClient.getActiveHealthOverrides({ systemIds });
536
+ const mapped: Record<
537
+ string,
538
+ {
539
+ status: "degraded" | "unhealthy";
540
+ source: string;
541
+ reason: string;
542
+ sourceId?: string;
543
+ }[]
544
+ > = {};
545
+ for (const [systemId, list] of Object.entries(overrides)) {
546
+ mapped[systemId] = list.map((o) => ({
547
+ status: o.status,
548
+ source: "incident",
549
+ reason: o.incidentTitle,
550
+ sourceId: o.incidentId,
551
+ }));
552
+ }
553
+ return mapped;
554
+ },
555
+ },
527
556
  recomputeSystemRollupHealth: (systemId) =>
528
557
  recomputeSystemRollupHealth({
529
558
  systemId,
@@ -1459,3 +1459,185 @@ describe("recomputeSystemRollupHealth", () => {
1459
1459
  expect(logger.error).toHaveBeenCalledTimes(1);
1460
1460
  });
1461
1461
  });
1462
+
1463
+ describe("executeHealthCheckJob - structured assertion outcomes", () => {
1464
+ it("stores _assertions per collector entry and downgrades on failure", async () => {
1465
+ const mockDb = createMockDb();
1466
+ const mockRegistry = createMockRegistry();
1467
+ const mockLogger = createMockLogger();
1468
+ const mockQueueManager = createMockQueueManager();
1469
+ const mockCatalogClient = createMockCatalogClient();
1470
+ const mockMaintenanceClient = createMockMaintenanceClient();
1471
+ const mockIncidentClient = createMockIncidentClient();
1472
+ const mockSignalService = createMockSignalService();
1473
+
1474
+ (mockCatalogClient.getSystem as any) = mock(async () => ({
1475
+ id: "system-1",
1476
+ name: "web-01",
1477
+ }));
1478
+
1479
+ // One collector entry with two assertions: isTrue passes, equals fails.
1480
+ let selectCallCount = 0;
1481
+ (mockDb.select as any) = mock(() => {
1482
+ selectCallCount++;
1483
+ if (selectCallCount === 2) {
1484
+ return {
1485
+ from: mock(() => ({
1486
+ innerJoin: mock(() => ({
1487
+ where: mock(() =>
1488
+ Promise.resolve([
1489
+ {
1490
+ configId: "config-1",
1491
+ configName: "checkout",
1492
+ strategyId: "test-strategy",
1493
+ config: { timeout: 5000 },
1494
+ collectors: [
1495
+ {
1496
+ id: "col-1",
1497
+ collectorId: "test-collector",
1498
+ config: {},
1499
+ assertions: [
1500
+ { field: "ok", operator: "isTrue" },
1501
+ { field: "code", operator: "equals", value: 200 },
1502
+ ],
1503
+ },
1504
+ ],
1505
+ interval: 45,
1506
+ enabled: true,
1507
+ paused: false,
1508
+ includeLocal: true,
1509
+ satelliteIds: [],
1510
+ },
1511
+ ]),
1512
+ ),
1513
+ })),
1514
+ })),
1515
+ };
1516
+ }
1517
+ return {
1518
+ from: mock(() => ({
1519
+ innerJoin: mock(() => ({
1520
+ where: mock(() => Promise.resolve([])),
1521
+ })),
1522
+ })),
1523
+ };
1524
+ });
1525
+
1526
+ // Capture every insert's values so we can find the stored run.
1527
+ const insertedValues: Record<string, unknown>[] = [];
1528
+ (mockDb.insert as any) = mock(() => ({
1529
+ values: mock((vals: Record<string, unknown>) => {
1530
+ insertedValues.push(vals);
1531
+ return Object.assign(Promise.resolve(), {
1532
+ onConflictDoUpdate: mock(() => Promise.resolve()),
1533
+ onConflictDoNothing: mock(() => Promise.resolve()),
1534
+ returning: mock(() => Promise.resolve([])),
1535
+ });
1536
+ }),
1537
+ }));
1538
+
1539
+ const mockCollectorRegistry = {
1540
+ register: mock(() => {}),
1541
+ getCollector: mock(() => ({
1542
+ collector: {
1543
+ id: "test-collector",
1544
+ execute: mock(async () => ({ result: { ok: true, code: 404 } })),
1545
+ config: new Versioned({ version: 1, schema: z.object({}) }),
1546
+ result: new Versioned({
1547
+ version: 1,
1548
+ schema: z.object({ ok: z.boolean(), code: z.number() }),
1549
+ }),
1550
+ mergeResult: mock(() => ({})),
1551
+ },
1552
+ })),
1553
+ getCollectors: mock(() => []),
1554
+ };
1555
+
1556
+ const queue =
1557
+ mockQueueManager.getQueue<HealthCheckJobPayload>("health-checks");
1558
+ let capturedHandler:
1559
+ | ((job: { data: HealthCheckJobPayload }) => Promise<void>)
1560
+ | undefined;
1561
+ (queue.consume as any) = mock(
1562
+ async (
1563
+ handler: (job: { data: HealthCheckJobPayload }) => Promise<void>,
1564
+ ) => {
1565
+ capturedHandler = handler;
1566
+ },
1567
+ );
1568
+
1569
+ await setupHealthCheckWorker({
1570
+ db: mockDb as unknown as Parameters<
1571
+ typeof setupHealthCheckWorker
1572
+ >[0]["db"],
1573
+ advisoryLock: mockAdvisoryLock,
1574
+ registry: mockRegistry,
1575
+ collectorRegistry: mockCollectorRegistry as unknown as Parameters<
1576
+ typeof setupHealthCheckWorker
1577
+ >[0]["collectorRegistry"],
1578
+ logger: mockLogger,
1579
+ queueManager: mockQueueManager,
1580
+ signalService: mockSignalService,
1581
+ catalogClient: mockCatalogClient as unknown as Parameters<
1582
+ typeof setupHealthCheckWorker
1583
+ >[0]["catalogClient"],
1584
+ notificationClient: {
1585
+ notifyForSubscription: () => Promise.resolve({ notifiedCount: 0 }),
1586
+ } as unknown as Parameters<
1587
+ typeof setupHealthCheckWorker
1588
+ >[0]["notificationClient"],
1589
+ maintenanceClient: mockMaintenanceClient as unknown as Parameters<
1590
+ typeof setupHealthCheckWorker
1591
+ >[0]["maintenanceClient"],
1592
+ incidentClient: mockIncidentClient as unknown as Parameters<
1593
+ typeof setupHealthCheckWorker
1594
+ >[0]["incidentClient"],
1595
+ getEmitHook: () => undefined,
1596
+ cache: passthroughCache,
1597
+ });
1598
+
1599
+ if (capturedHandler) {
1600
+ // Downstream aggregation touches DB surfaces the lightweight mock
1601
+ // doesn't model; the run insert we assert on happens before that.
1602
+ await capturedHandler({
1603
+ data: { configId: "config-1", systemId: "system-1" },
1604
+ }).catch(() => {});
1605
+ }
1606
+
1607
+ const runInsert = insertedValues.find(
1608
+ (vals) => "status" in vals && "result" in vals,
1609
+ );
1610
+ expect(runInsert).toBeDefined();
1611
+ // The failed `code equals 200` assertion downgrades the run.
1612
+ expect(runInsert?.status).toBe("unhealthy");
1613
+
1614
+ const runResult = runInsert?.result as {
1615
+ metadata: {
1616
+ collectors: Record<
1617
+ string,
1618
+ {
1619
+ _assertionFailed?: string;
1620
+ _assertions?: {
1621
+ field: string;
1622
+ passed: boolean;
1623
+ actual?: string;
1624
+ }[];
1625
+ }
1626
+ >;
1627
+ };
1628
+ };
1629
+ const entry = runResult.metadata.collectors["col-1"];
1630
+ expect(entry._assertionFailed).toBe("code equals 200");
1631
+ expect(entry._assertions?.length).toBe(2);
1632
+ expect(entry._assertions?.[0]).toMatchObject({
1633
+ field: "ok",
1634
+ passed: true,
1635
+ actual: "true",
1636
+ });
1637
+ expect(entry._assertions?.[1]).toMatchObject({
1638
+ field: "code",
1639
+ passed: false,
1640
+ actual: "404",
1641
+ });
1642
+ });
1643
+ });
@@ -60,7 +60,8 @@ import {
60
60
  shouldNotifyTransition,
61
61
  } from "./notification-policy";
62
62
  import { recordStateTransition } from "./state-transitions";
63
- import { evaluateCollectorAssertions } from "./collector-assertions";
63
+ import { evaluateCollectorAssertionOutcomes } from "./collector-assertions";
64
+ import type { AssertionOutcome } from "@checkstack/healthcheck-common";
64
65
  import {
65
66
  writeHealthEntity,
66
67
  createHealthEntitySerializer,
@@ -947,13 +948,18 @@ async function executeHealthCheckJob(props: {
947
948
  collectorError = collectorResult.error;
948
949
  }
949
950
 
950
- // Evaluate per-collector assertions (plain fields + JSONPath)
951
+ // Evaluate per-collector assertions (plain fields + JSONPath).
952
+ // ALL outcomes are stored (pass AND fail) so assertions are
953
+ // analyzable over time, not only visible on failure.
951
954
  let assertionFailed: string | undefined;
955
+ let assertionOutcomes: AssertionOutcome[] = [];
952
956
  if (collectorResult.result) {
953
- assertionFailed = evaluateCollectorAssertions({
957
+ const evaluation = evaluateCollectorAssertionOutcomes({
954
958
  assertions: collectorEntry.assertions,
955
959
  result: collectorResult.result as Record<string, unknown>,
956
960
  });
961
+ assertionFailed = evaluation.firstFailureMessage;
962
+ assertionOutcomes = evaluation.outcomes;
957
963
  if (assertionFailed) {
958
964
  logger.debug(
959
965
  `Collector ${storageKey} assertion failed: ${assertionFailed}`,
@@ -977,6 +983,9 @@ async function executeHealthCheckJob(props: {
977
983
  _collectorId: collectorEntry.collectorId,
978
984
  _assertionFailed: assertionFailed,
979
985
  _collectorError: collectorError,
986
+ ...(assertionOutcomes.length > 0
987
+ ? { _assertions: assertionOutcomes }
988
+ : {}),
980
989
  ...strippedResult,
981
990
  },
982
991
  };
@@ -5,6 +5,7 @@ import {
5
5
  deserializeTDigest,
6
6
  incrementHourlyAggregate,
7
7
  } from "./realtime-aggregation";
8
+ import { computeAssertionKey } from "@checkstack/healthcheck-common";
8
9
  import { TDigest } from "tdigest";
9
10
 
10
11
  describe("getHourBucketStart", () => {
@@ -494,3 +495,138 @@ describe("incrementHourlyAggregate", () => {
494
495
  expect(inserted.maxLatencyMs).toBe(150);
495
496
  });
496
497
  });
498
+
499
+ describe("incrementHourlyAggregate - assertion stats folding", () => {
500
+ const KEY = computeAssertionKey({
501
+ assertion: { field: "statusCode", operator: "equals", value: 200 },
502
+ });
503
+
504
+ const outcome = (passed: boolean) => ({
505
+ key: KEY,
506
+ field: "statusCode",
507
+ operator: "equals",
508
+ value: "200",
509
+ passed,
510
+ });
511
+
512
+ const collectorRegistry = {
513
+ register: mock(() => {}),
514
+ getCollector: mock(() => ({
515
+ collector: {
516
+ id: "test-collector",
517
+ mergeResult: mock(() => ({ count: 1 })),
518
+ },
519
+ })),
520
+ getCollectors: mock(() => []),
521
+ } as unknown as Parameters<
522
+ typeof incrementHourlyAggregate
523
+ >[0]["collectorRegistry"];
524
+
525
+ function runResult(outcomes: unknown[]) {
526
+ return {
527
+ metadata: {
528
+ collectors: {
529
+ "uuid-1": {
530
+ _collectorId: "test-collector",
531
+ _assertions: outcomes,
532
+ statusCode: 200,
533
+ },
534
+ },
535
+ },
536
+ };
537
+ }
538
+
539
+ it("folds a run's outcomes into the bucket's per-assertion counts", async () => {
540
+ let inserted: Record<string, unknown> | undefined;
541
+ const db = {
542
+ select: mock(() => ({
543
+ from: mock(() => ({
544
+ where: mock(() => ({
545
+ limit: mock(() => Promise.resolve([])),
546
+ })),
547
+ })),
548
+ })),
549
+ insert: mock(() => ({
550
+ values: mock((values: Record<string, unknown>) => {
551
+ inserted = values;
552
+ return { onConflictDoUpdate: mock(() => Promise.resolve()) };
553
+ }),
554
+ })),
555
+ };
556
+
557
+ await incrementHourlyAggregate({
558
+ db: db as never,
559
+ systemId: "sys-1",
560
+ configurationId: "config-1",
561
+ status: "unhealthy",
562
+ latencyMs: 100,
563
+ runTimestamp: new Date("2024-01-15T10:35:00Z"),
564
+ result: runResult([outcome(true), outcome(false)]) as never,
565
+ collectorRegistry,
566
+ });
567
+
568
+ const aggregated = inserted?.aggregatedResult as Record<string, unknown>;
569
+ expect(aggregated.assertions).toEqual({
570
+ "uuid-1": { [KEY]: { passCount: 1, failCount: 1 } },
571
+ });
572
+ // The internal _assertions never leak into collector merge output.
573
+ const collectors = aggregated.collectors as Record<
574
+ string,
575
+ Record<string, unknown>
576
+ >;
577
+ expect(collectors["uuid-1"]._assertions).toBeUndefined();
578
+ });
579
+
580
+ it("increments existing counts and keeps distinct keys separate (mid-bucket config edit)", async () => {
581
+ const OTHER_KEY = computeAssertionKey({
582
+ assertion: { field: "statusCode", operator: "equals", value: 201 },
583
+ });
584
+ const existing = {
585
+ tdigestState: null,
586
+ minLatencyMs: null,
587
+ maxLatencyMs: null,
588
+ aggregatedResult: {
589
+ collectors: {},
590
+ assertions: { "uuid-1": { [KEY]: { passCount: 4, failCount: 0 } } },
591
+ },
592
+ };
593
+ let inserted: Record<string, unknown> | undefined;
594
+ const db = {
595
+ select: mock(() => ({
596
+ from: mock(() => ({
597
+ where: mock(() => ({
598
+ limit: mock(() => Promise.resolve([existing])),
599
+ })),
600
+ })),
601
+ })),
602
+ insert: mock(() => ({
603
+ values: mock((values: Record<string, unknown>) => {
604
+ inserted = values;
605
+ return { onConflictDoUpdate: mock(() => Promise.resolve()) };
606
+ }),
607
+ })),
608
+ };
609
+
610
+ // The edited assertion (value 201) starts a NEW series in the same bucket.
611
+ await incrementHourlyAggregate({
612
+ db: db as never,
613
+ systemId: "sys-1",
614
+ configurationId: "config-1",
615
+ status: "healthy",
616
+ latencyMs: 100,
617
+ runTimestamp: new Date("2024-01-15T10:40:00Z"),
618
+ result: runResult([
619
+ { ...outcome(true), key: OTHER_KEY, value: "201" },
620
+ ]) as never,
621
+ collectorRegistry,
622
+ });
623
+
624
+ const aggregated = inserted?.aggregatedResult as Record<string, unknown>;
625
+ expect(aggregated.assertions).toEqual({
626
+ "uuid-1": {
627
+ [KEY]: { passCount: 4, failCount: 0 },
628
+ [OTHER_KEY]: { passCount: 1, failCount: 0 },
629
+ },
630
+ });
631
+ });
632
+ });