autotel 4.1.0 → 4.2.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.
- package/package.json +1 -2
- package/src/attribute-redacting-processor.test.ts +0 -763
- package/src/attribute-redacting-processor.ts +0 -621
- package/src/attributes/attachers.ts +0 -161
- package/src/attributes/builders.ts +0 -529
- package/src/attributes/domains.ts +0 -42
- package/src/attributes/index.ts +0 -81
- package/src/attributes/registry.ts +0 -323
- package/src/attributes/types.ts +0 -211
- package/src/attributes/utils.ts +0 -64
- package/src/attributes/validators.ts +0 -266
- package/src/attributes.test.ts +0 -292
- package/src/auto.ts +0 -67
- package/src/autotel-logger.test.ts +0 -548
- package/src/autotel-logger.ts +0 -364
- package/src/baggage-span-processor.test.ts +0 -202
- package/src/baggage-span-processor.ts +0 -100
- package/src/business-baggage.test.ts +0 -500
- package/src/business-baggage.ts +0 -669
- package/src/circuit-breaker.test.ts +0 -341
- package/src/circuit-breaker.ts +0 -184
- package/src/config.test.ts +0 -94
- package/src/config.ts +0 -172
- package/src/correlated-events.test.ts +0 -151
- package/src/correlated-events.ts +0 -47
- package/src/correlation-id.test.ts +0 -163
- package/src/correlation-id.ts +0 -206
- package/src/db.test.ts +0 -252
- package/src/db.ts +0 -447
- package/src/decorators.test.ts +0 -153
- package/src/decorators.ts +0 -188
- package/src/define-event.test.ts +0 -41
- package/src/define-event.ts +0 -58
- package/src/devtools.ts +0 -60
- package/src/drain-pipeline.test.ts +0 -68
- package/src/drain-pipeline.ts +0 -199
- package/src/drain-toolkit.test.ts +0 -113
- package/src/drain-toolkit.ts +0 -129
- package/src/enricher-toolkit.test.ts +0 -67
- package/src/enricher-toolkit.ts +0 -79
- package/src/enrichers.test.ts +0 -150
- package/src/enrichers.ts +0 -145
- package/src/env-config.test.ts +0 -323
- package/src/env-config.ts +0 -309
- package/src/error-catalog.test.ts +0 -133
- package/src/error-catalog.ts +0 -262
- package/src/event-queue.test.ts +0 -864
- package/src/event-queue.ts +0 -699
- package/src/event-subscriber.ts +0 -262
- package/src/event-testing.ts +0 -197
- package/src/event.test.ts +0 -1104
- package/src/event.ts +0 -988
- package/src/events-config.ts +0 -235
- package/src/exporters.ts +0 -165
- package/src/filtering-span-processor.test.ts +0 -281
- package/src/filtering-span-processor.ts +0 -111
- package/src/flatten-attributes.test.ts +0 -76
- package/src/flatten-attributes.ts +0 -80
- package/src/functional.strict-types.typecheck.ts +0 -53
- package/src/functional.test.ts +0 -1464
- package/src/functional.ts +0 -2539
- package/src/functional.types.test.ts +0 -135
- package/src/hook.mjs +0 -15
- package/src/http.test.ts +0 -485
- package/src/http.ts +0 -424
- package/src/index.ts +0 -433
- package/src/init-auto-redactor.test.ts +0 -53
- package/src/init-redactor.test.ts +0 -8
- package/src/init.customization.test.ts +0 -665
- package/src/init.integrations.test.ts +0 -399
- package/src/init.openllmetry.test.ts +0 -194
- package/src/init.protocol.test.ts +0 -215
- package/src/init.ts +0 -2439
- package/src/instrumentation.test.ts +0 -108
- package/src/instrumentation.ts +0 -319
- package/src/logger.test.ts +0 -125
- package/src/logger.ts +0 -341
- package/src/messaging-adapters.test.ts +0 -595
- package/src/messaging-adapters.ts +0 -583
- package/src/messaging-testing.test.ts +0 -573
- package/src/messaging-testing.ts +0 -935
- package/src/messaging.test.ts +0 -1646
- package/src/messaging.ts +0 -2245
- package/src/metric-helpers.ts +0 -47
- package/src/metric-testing.ts +0 -197
- package/src/metric.ts +0 -446
- package/src/metrics.test.ts +0 -241
- package/src/node-require.ts +0 -123
- package/src/operation-context.ts +0 -93
- package/src/parse-error.test.ts +0 -73
- package/src/parse-error.ts +0 -112
- package/src/posthog-logs.test.ts +0 -115
- package/src/posthog-logs.ts +0 -77
- package/src/pretty-console-exporter.test.ts +0 -545
- package/src/pretty-console-exporter.ts +0 -413
- package/src/pretty-log-formatter.test.ts +0 -123
- package/src/pretty-log-formatter.ts +0 -210
- package/src/processors/canonical-log-line-processor.test.ts +0 -523
- package/src/processors/canonical-log-line-processor.ts +0 -396
- package/src/processors.ts +0 -152
- package/src/rate-limiter.test.ts +0 -199
- package/src/rate-limiter.ts +0 -98
- package/src/redact-values.test.ts +0 -90
- package/src/redact-values.ts +0 -34
- package/src/register.ts +0 -37
- package/src/request-logger.test.ts +0 -545
- package/src/request-logger.ts +0 -342
- package/src/sampling.test.ts +0 -1060
- package/src/sampling.ts +0 -737
- package/src/security-schema.test.ts +0 -45
- package/src/security-schema.ts +0 -107
- package/src/semantic-conventions.ts +0 -15
- package/src/semantic-helpers.test.ts +0 -226
- package/src/semantic-helpers.ts +0 -438
- package/src/shutdown.test.ts +0 -364
- package/src/shutdown.ts +0 -246
- package/src/span-name-normalizer.test.ts +0 -377
- package/src/span-name-normalizer.ts +0 -213
- package/src/stable-hash.ts +0 -27
- package/src/structured-error.test.ts +0 -191
- package/src/structured-error.ts +0 -157
- package/src/stub.integration.test.ts +0 -361
- package/src/tail-sampling-processor.test.ts +0 -230
- package/src/tail-sampling-processor.ts +0 -55
- package/src/test-span-collector.test.ts +0 -234
- package/src/test-span-collector.ts +0 -150
- package/src/testing.ts +0 -705
- package/src/trace-context.test.ts +0 -73
- package/src/trace-context.ts +0 -567
- package/src/trace-helpers.new.test.ts +0 -278
- package/src/trace-helpers.test.ts +0 -290
- package/src/trace-helpers.ts +0 -710
- package/src/trace-hybrid.test.ts +0 -42
- package/src/trace-hybrid.ts +0 -37
- package/src/tracer-provider.test.ts +0 -183
- package/src/tracer-provider.ts +0 -266
- package/src/track.test.ts +0 -154
- package/src/track.ts +0 -216
- package/src/validate.test.ts +0 -287
- package/src/validate.ts +0 -307
- package/src/validation-attributes.ts +0 -43
- package/src/validation.test.ts +0 -330
- package/src/validation.ts +0 -246
- package/src/variable-name-inference.test.ts +0 -178
- package/src/variable-name-inference.ts +0 -242
- package/src/webhook.test.ts +0 -649
- package/src/webhook.ts +0 -637
- package/src/workflow-distributed.test.ts +0 -786
- package/src/workflow-distributed.ts +0 -916
- package/src/workflow.async-safety.integration.test.ts +0 -345
- package/src/workflow.test.ts +0 -647
- package/src/workflow.ts +0 -810
- package/src/yaml-config.test.ts +0 -373
- package/src/yaml-config.ts +0 -351
package/src/messaging-testing.ts
DELETED
|
@@ -1,935 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Testing utilities for messaging instrumentation
|
|
3
|
-
*
|
|
4
|
-
* Provides mock producers, consumers, and assertion helpers
|
|
5
|
-
* for testing event-driven code with Autotel's messaging module.
|
|
6
|
-
*
|
|
7
|
-
* @example Basic test setup
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { createMessagingTestHarness } from 'autotel/messaging-testing';
|
|
10
|
-
*
|
|
11
|
-
* describe('Order processing', () => {
|
|
12
|
-
* const harness = createMessagingTestHarness();
|
|
13
|
-
*
|
|
14
|
-
* beforeEach(() => harness.reset());
|
|
15
|
-
* afterAll(() => harness.shutdown());
|
|
16
|
-
*
|
|
17
|
-
* it('should process order and publish event', async () => {
|
|
18
|
-
* await processOrder({ id: 'order-123' });
|
|
19
|
-
*
|
|
20
|
-
* harness.assertProducerCalled('orders', {
|
|
21
|
-
* messageCount: 1,
|
|
22
|
-
* hasTraceHeaders: true,
|
|
23
|
-
* });
|
|
24
|
-
* });
|
|
25
|
-
* });
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @module
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import type { Link, SpanContext } from '@opentelemetry/api';
|
|
32
|
-
import type {
|
|
33
|
-
RebalanceEvent,
|
|
34
|
-
PartitionAssignment,
|
|
35
|
-
OutOfOrderInfo,
|
|
36
|
-
} from './messaging';
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Types
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Recorded producer call
|
|
44
|
-
*/
|
|
45
|
-
export interface RecordedProducerCall {
|
|
46
|
-
/** Destination (topic/queue) */
|
|
47
|
-
destination: string;
|
|
48
|
-
|
|
49
|
-
/** System (kafka, sqs, etc.) */
|
|
50
|
-
system: string;
|
|
51
|
-
|
|
52
|
-
/** Message payload */
|
|
53
|
-
payload: unknown;
|
|
54
|
-
|
|
55
|
-
/** Headers injected */
|
|
56
|
-
headers: Record<string, string>;
|
|
57
|
-
|
|
58
|
-
/** Timestamp of call */
|
|
59
|
-
timestamp: number;
|
|
60
|
-
|
|
61
|
-
/** Trace ID from headers */
|
|
62
|
-
traceId?: string;
|
|
63
|
-
|
|
64
|
-
/** Span ID from headers */
|
|
65
|
-
spanId?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Recorded consumer call
|
|
70
|
-
*/
|
|
71
|
-
export interface RecordedConsumerCall {
|
|
72
|
-
/** Destination (topic/queue) */
|
|
73
|
-
destination: string;
|
|
74
|
-
|
|
75
|
-
/** System (kafka, sqs, etc.) */
|
|
76
|
-
system: string;
|
|
77
|
-
|
|
78
|
-
/** Consumer group */
|
|
79
|
-
consumerGroup?: string;
|
|
80
|
-
|
|
81
|
-
/** Message payload */
|
|
82
|
-
payload: unknown;
|
|
83
|
-
|
|
84
|
-
/** Headers extracted */
|
|
85
|
-
headers?: Record<string, string>;
|
|
86
|
-
|
|
87
|
-
/** Timestamp of call */
|
|
88
|
-
timestamp: number;
|
|
89
|
-
|
|
90
|
-
/** Producer links extracted */
|
|
91
|
-
producerLinks: Link[];
|
|
92
|
-
|
|
93
|
-
/** Whether message was duplicate */
|
|
94
|
-
isDuplicate: boolean;
|
|
95
|
-
|
|
96
|
-
/** Out of order info if detected */
|
|
97
|
-
outOfOrderInfo: OutOfOrderInfo | null;
|
|
98
|
-
|
|
99
|
-
/** DLQ reason if routed to DLQ */
|
|
100
|
-
dlqReason?: string;
|
|
101
|
-
|
|
102
|
-
/** Retry attempt number */
|
|
103
|
-
retryAttempt?: number;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Recorded rebalance event
|
|
108
|
-
*/
|
|
109
|
-
export interface RecordedRebalanceEvent extends RebalanceEvent {
|
|
110
|
-
/** Destination (topic) */
|
|
111
|
-
destination: string;
|
|
112
|
-
|
|
113
|
-
/** Consumer group */
|
|
114
|
-
consumerGroup: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Mock message for testing
|
|
119
|
-
*/
|
|
120
|
-
export interface MockMessage<T = unknown> {
|
|
121
|
-
/** Message payload */
|
|
122
|
-
payload: T;
|
|
123
|
-
|
|
124
|
-
/** Headers */
|
|
125
|
-
headers?: Record<string, string>;
|
|
126
|
-
|
|
127
|
-
/** Offset/sequence number */
|
|
128
|
-
offset?: number;
|
|
129
|
-
|
|
130
|
-
/** Partition */
|
|
131
|
-
partition?: number;
|
|
132
|
-
|
|
133
|
-
/** Key */
|
|
134
|
-
key?: string;
|
|
135
|
-
|
|
136
|
-
/** Message ID */
|
|
137
|
-
messageId?: string;
|
|
138
|
-
|
|
139
|
-
/** Timestamp */
|
|
140
|
-
timestamp?: number;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Producer assertion options
|
|
145
|
-
*/
|
|
146
|
-
export interface ProducerAssertionOptions {
|
|
147
|
-
/** Expected number of messages */
|
|
148
|
-
messageCount?: number;
|
|
149
|
-
|
|
150
|
-
/** Whether trace headers should be present */
|
|
151
|
-
hasTraceHeaders?: boolean;
|
|
152
|
-
|
|
153
|
-
/** Expected destination */
|
|
154
|
-
destination?: string;
|
|
155
|
-
|
|
156
|
-
/** Custom matcher for payload */
|
|
157
|
-
payloadMatcher?: (payload: unknown) => boolean;
|
|
158
|
-
|
|
159
|
-
/** Expected trace ID */
|
|
160
|
-
traceId?: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Consumer assertion options
|
|
165
|
-
*/
|
|
166
|
-
export interface ConsumerAssertionOptions {
|
|
167
|
-
/** Expected number of messages processed */
|
|
168
|
-
messageCount?: number;
|
|
169
|
-
|
|
170
|
-
/** Whether producer links should be present */
|
|
171
|
-
hasProducerLinks?: boolean;
|
|
172
|
-
|
|
173
|
-
/** Expected destination */
|
|
174
|
-
destination?: string;
|
|
175
|
-
|
|
176
|
-
/** Expected consumer group */
|
|
177
|
-
consumerGroup?: string;
|
|
178
|
-
|
|
179
|
-
/** Whether any messages were duplicates */
|
|
180
|
-
hasDuplicates?: boolean;
|
|
181
|
-
|
|
182
|
-
/** Whether any messages were out of order */
|
|
183
|
-
hasOutOfOrder?: boolean;
|
|
184
|
-
|
|
185
|
-
/** Whether any messages went to DLQ */
|
|
186
|
-
hasDLQ?: boolean;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Messaging test harness
|
|
191
|
-
*/
|
|
192
|
-
export interface MessagingTestHarness {
|
|
193
|
-
/** All recorded producer calls */
|
|
194
|
-
producerCalls: RecordedProducerCall[];
|
|
195
|
-
|
|
196
|
-
/** All recorded consumer calls */
|
|
197
|
-
consumerCalls: RecordedConsumerCall[];
|
|
198
|
-
|
|
199
|
-
/** All recorded rebalance events */
|
|
200
|
-
rebalanceEvents: RecordedRebalanceEvent[];
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Record a producer call
|
|
204
|
-
*/
|
|
205
|
-
recordProducerCall(call: Omit<RecordedProducerCall, 'timestamp'>): void;
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Record a consumer call
|
|
209
|
-
*/
|
|
210
|
-
recordConsumerCall(call: Omit<RecordedConsumerCall, 'timestamp'>): void;
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Record a rebalance event
|
|
214
|
-
*/
|
|
215
|
-
recordRebalanceEvent(event: RecordedRebalanceEvent): void;
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Create a mock message with trace headers
|
|
219
|
-
*/
|
|
220
|
-
createMockMessage<T>(
|
|
221
|
-
payload: T,
|
|
222
|
-
options?: Partial<MockMessage<T>>,
|
|
223
|
-
): MockMessage<T>;
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Create mock trace headers
|
|
227
|
-
*/
|
|
228
|
-
createMockTraceHeaders(
|
|
229
|
-
traceId?: string,
|
|
230
|
-
spanId?: string,
|
|
231
|
-
): Record<string, string>;
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Assert producer was called with expected options
|
|
235
|
-
*/
|
|
236
|
-
assertProducerCalled(
|
|
237
|
-
destination: string,
|
|
238
|
-
options?: ProducerAssertionOptions,
|
|
239
|
-
): void;
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Assert producer was not called
|
|
243
|
-
*/
|
|
244
|
-
assertProducerNotCalled(destination?: string): void;
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Assert consumer processed messages with expected options
|
|
248
|
-
*/
|
|
249
|
-
assertConsumerProcessed(
|
|
250
|
-
destination: string,
|
|
251
|
-
options?: ConsumerAssertionOptions,
|
|
252
|
-
): void;
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Assert consumer was not called
|
|
256
|
-
*/
|
|
257
|
-
assertConsumerNotCalled(destination?: string): void;
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Assert rebalance occurred
|
|
261
|
-
*/
|
|
262
|
-
assertRebalanceOccurred(
|
|
263
|
-
destination: string,
|
|
264
|
-
type: RebalanceEvent['type'],
|
|
265
|
-
partitionCount?: number,
|
|
266
|
-
): void;
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Get producer calls for destination
|
|
270
|
-
*/
|
|
271
|
-
getProducerCalls(destination?: string): RecordedProducerCall[];
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Get consumer calls for destination
|
|
275
|
-
*/
|
|
276
|
-
getConsumerCalls(destination?: string): RecordedConsumerCall[];
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Get the last producer call
|
|
280
|
-
*/
|
|
281
|
-
getLastProducerCall(destination?: string): RecordedProducerCall | undefined;
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Get the last consumer call
|
|
285
|
-
*/
|
|
286
|
-
getLastConsumerCall(destination?: string): RecordedConsumerCall | undefined;
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Reset all recorded calls
|
|
290
|
-
*/
|
|
291
|
-
reset(): void;
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Shutdown the harness
|
|
295
|
-
*/
|
|
296
|
-
shutdown(): void;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// ============================================================================
|
|
300
|
-
// Implementation
|
|
301
|
-
// ============================================================================
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Generate a random hex string
|
|
305
|
-
*/
|
|
306
|
-
function randomHex(length: number): string {
|
|
307
|
-
let result = '';
|
|
308
|
-
const chars = '0123456789abcdef';
|
|
309
|
-
for (let i = 0; i < length; i++) {
|
|
310
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
311
|
-
}
|
|
312
|
-
return result;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Create a messaging test harness
|
|
317
|
-
*
|
|
318
|
-
* Provides utilities for recording and asserting on producer/consumer calls
|
|
319
|
-
* during testing.
|
|
320
|
-
*
|
|
321
|
-
* @example
|
|
322
|
-
* ```typescript
|
|
323
|
-
* const harness = createMessagingTestHarness();
|
|
324
|
-
*
|
|
325
|
-
* // In your test setup
|
|
326
|
-
* beforeEach(() => harness.reset());
|
|
327
|
-
*
|
|
328
|
-
* // In your tests
|
|
329
|
-
* it('should publish order event', async () => {
|
|
330
|
-
* await orderService.createOrder({ id: '123' });
|
|
331
|
-
*
|
|
332
|
-
* harness.assertProducerCalled('orders', {
|
|
333
|
-
* messageCount: 1,
|
|
334
|
-
* hasTraceHeaders: true,
|
|
335
|
-
* });
|
|
336
|
-
*
|
|
337
|
-
* const lastCall = harness.getLastProducerCall('orders');
|
|
338
|
-
* expect(lastCall?.payload).toMatchObject({ orderId: '123' });
|
|
339
|
-
* });
|
|
340
|
-
* ```
|
|
341
|
-
*/
|
|
342
|
-
export function createMessagingTestHarness(): MessagingTestHarness {
|
|
343
|
-
const producerCalls: RecordedProducerCall[] = [];
|
|
344
|
-
const consumerCalls: RecordedConsumerCall[] = [];
|
|
345
|
-
const rebalanceEvents: RecordedRebalanceEvent[] = [];
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
producerCalls,
|
|
349
|
-
consumerCalls,
|
|
350
|
-
rebalanceEvents,
|
|
351
|
-
|
|
352
|
-
recordProducerCall(call) {
|
|
353
|
-
producerCalls.push({
|
|
354
|
-
...call,
|
|
355
|
-
timestamp: Date.now(),
|
|
356
|
-
});
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
recordConsumerCall(call) {
|
|
360
|
-
consumerCalls.push({
|
|
361
|
-
...call,
|
|
362
|
-
timestamp: Date.now(),
|
|
363
|
-
});
|
|
364
|
-
},
|
|
365
|
-
|
|
366
|
-
recordRebalanceEvent(event) {
|
|
367
|
-
rebalanceEvents.push(event);
|
|
368
|
-
},
|
|
369
|
-
|
|
370
|
-
createMockMessage<T>(
|
|
371
|
-
payload: T,
|
|
372
|
-
options: Partial<MockMessage<T>> = {},
|
|
373
|
-
): MockMessage<T> {
|
|
374
|
-
return {
|
|
375
|
-
payload,
|
|
376
|
-
headers: options.headers ?? this.createMockTraceHeaders(),
|
|
377
|
-
offset: options.offset ?? Math.floor(Math.random() * 10_000),
|
|
378
|
-
partition: options.partition ?? 0,
|
|
379
|
-
key: options.key,
|
|
380
|
-
messageId: options.messageId ?? `msg-${randomHex(8)}`,
|
|
381
|
-
timestamp: options.timestamp ?? Date.now(),
|
|
382
|
-
};
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
createMockTraceHeaders(
|
|
386
|
-
traceId?: string,
|
|
387
|
-
spanId?: string,
|
|
388
|
-
): Record<string, string> {
|
|
389
|
-
const tid = traceId ?? randomHex(32);
|
|
390
|
-
const sid = spanId ?? randomHex(16);
|
|
391
|
-
return {
|
|
392
|
-
traceparent: `00-${tid}-${sid}-01`,
|
|
393
|
-
};
|
|
394
|
-
},
|
|
395
|
-
|
|
396
|
-
assertProducerCalled(
|
|
397
|
-
destination: string,
|
|
398
|
-
options: ProducerAssertionOptions = {},
|
|
399
|
-
) {
|
|
400
|
-
const calls = producerCalls.filter((c) => c.destination === destination);
|
|
401
|
-
|
|
402
|
-
if (calls.length === 0) {
|
|
403
|
-
throw new Error(
|
|
404
|
-
`Expected producer to be called for destination '${destination}', but it was not called`,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
options.messageCount !== undefined &&
|
|
410
|
-
calls.length !== options.messageCount
|
|
411
|
-
) {
|
|
412
|
-
throw new Error(
|
|
413
|
-
`Expected ${options.messageCount} producer calls for '${destination}', got ${calls.length}`,
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (options.hasTraceHeaders) {
|
|
418
|
-
const withoutHeaders = calls.filter((c) => !c.headers?.traceparent);
|
|
419
|
-
if (withoutHeaders.length > 0) {
|
|
420
|
-
throw new Error(
|
|
421
|
-
`Expected all producer calls for '${destination}' to have trace headers, but ${withoutHeaders.length} did not`,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (options.traceId) {
|
|
427
|
-
const matchingTraceId = calls.filter(
|
|
428
|
-
(c) => c.traceId === options.traceId,
|
|
429
|
-
);
|
|
430
|
-
if (matchingTraceId.length === 0) {
|
|
431
|
-
throw new Error(
|
|
432
|
-
`Expected producer call for '${destination}' with traceId '${options.traceId}', but none found`,
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (options.payloadMatcher) {
|
|
438
|
-
const matching = calls.filter((c) =>
|
|
439
|
-
options.payloadMatcher!(c.payload),
|
|
440
|
-
);
|
|
441
|
-
if (matching.length === 0) {
|
|
442
|
-
throw new Error(
|
|
443
|
-
`Expected producer call for '${destination}' to match payload matcher, but none did`,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
},
|
|
448
|
-
|
|
449
|
-
assertProducerNotCalled(destination?: string) {
|
|
450
|
-
if (destination) {
|
|
451
|
-
const calls = producerCalls.filter(
|
|
452
|
-
(c) => c.destination === destination,
|
|
453
|
-
);
|
|
454
|
-
if (calls.length > 0) {
|
|
455
|
-
throw new Error(
|
|
456
|
-
`Expected producer not to be called for '${destination}', but it was called ${calls.length} times`,
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
if (producerCalls.length > 0) {
|
|
461
|
-
throw new Error(
|
|
462
|
-
`Expected no producer calls, but ${producerCalls.length} calls were made`,
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
},
|
|
467
|
-
|
|
468
|
-
assertConsumerProcessed(
|
|
469
|
-
destination: string,
|
|
470
|
-
options: ConsumerAssertionOptions = {},
|
|
471
|
-
) {
|
|
472
|
-
const calls = consumerCalls.filter((c) => c.destination === destination);
|
|
473
|
-
|
|
474
|
-
if (calls.length === 0) {
|
|
475
|
-
throw new Error(
|
|
476
|
-
`Expected consumer to process messages for destination '${destination}', but none were processed`,
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
options.messageCount !== undefined &&
|
|
482
|
-
calls.length !== options.messageCount
|
|
483
|
-
) {
|
|
484
|
-
throw new Error(
|
|
485
|
-
`Expected ${options.messageCount} consumer calls for '${destination}', got ${calls.length}`,
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (options.consumerGroup) {
|
|
490
|
-
const wrongGroup = calls.filter(
|
|
491
|
-
(c) => c.consumerGroup !== options.consumerGroup,
|
|
492
|
-
);
|
|
493
|
-
if (wrongGroup.length > 0) {
|
|
494
|
-
throw new Error(
|
|
495
|
-
`Expected consumer group '${options.consumerGroup}' for '${destination}', but found different groups`,
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (options.hasProducerLinks) {
|
|
501
|
-
const withoutLinks = calls.filter((c) => c.producerLinks.length === 0);
|
|
502
|
-
if (withoutLinks.length > 0) {
|
|
503
|
-
throw new Error(
|
|
504
|
-
`Expected all consumer calls for '${destination}' to have producer links, but ${withoutLinks.length} did not`,
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (options.hasDuplicates !== undefined) {
|
|
510
|
-
const duplicates = calls.filter((c) => c.isDuplicate);
|
|
511
|
-
if (options.hasDuplicates && duplicates.length === 0) {
|
|
512
|
-
throw new Error(
|
|
513
|
-
`Expected duplicate messages for '${destination}', but none were detected`,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
if (!options.hasDuplicates && duplicates.length > 0) {
|
|
517
|
-
throw new Error(
|
|
518
|
-
`Expected no duplicate messages for '${destination}', but ${duplicates.length} were detected`,
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (options.hasOutOfOrder !== undefined) {
|
|
524
|
-
const outOfOrder = calls.filter((c) => c.outOfOrderInfo !== null);
|
|
525
|
-
if (options.hasOutOfOrder && outOfOrder.length === 0) {
|
|
526
|
-
throw new Error(
|
|
527
|
-
`Expected out-of-order messages for '${destination}', but none were detected`,
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
if (!options.hasOutOfOrder && outOfOrder.length > 0) {
|
|
531
|
-
throw new Error(
|
|
532
|
-
`Expected no out-of-order messages for '${destination}', but ${outOfOrder.length} were detected`,
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (options.hasDLQ !== undefined) {
|
|
538
|
-
const dlqCalls = calls.filter((c) => c.dlqReason !== undefined);
|
|
539
|
-
if (options.hasDLQ && dlqCalls.length === 0) {
|
|
540
|
-
throw new Error(
|
|
541
|
-
`Expected DLQ routing for '${destination}', but none occurred`,
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
if (!options.hasDLQ && dlqCalls.length > 0) {
|
|
545
|
-
throw new Error(
|
|
546
|
-
`Expected no DLQ routing for '${destination}', but ${dlqCalls.length} occurred`,
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
|
|
552
|
-
assertConsumerNotCalled(destination?: string) {
|
|
553
|
-
if (destination) {
|
|
554
|
-
const calls = consumerCalls.filter(
|
|
555
|
-
(c) => c.destination === destination,
|
|
556
|
-
);
|
|
557
|
-
if (calls.length > 0) {
|
|
558
|
-
throw new Error(
|
|
559
|
-
`Expected consumer not to be called for '${destination}', but it processed ${calls.length} messages`,
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
} else {
|
|
563
|
-
if (consumerCalls.length > 0) {
|
|
564
|
-
throw new Error(
|
|
565
|
-
`Expected no consumer calls, but ${consumerCalls.length} messages were processed`,
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
},
|
|
570
|
-
|
|
571
|
-
assertRebalanceOccurred(
|
|
572
|
-
destination: string,
|
|
573
|
-
type: RebalanceEvent['type'],
|
|
574
|
-
partitionCount?: number,
|
|
575
|
-
) {
|
|
576
|
-
const events = rebalanceEvents.filter(
|
|
577
|
-
(e) => e.destination === destination && e.type === type,
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
if (events.length === 0) {
|
|
581
|
-
throw new Error(
|
|
582
|
-
`Expected rebalance '${type}' for '${destination}', but none occurred`,
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
if (partitionCount !== undefined) {
|
|
587
|
-
const matching = events.filter(
|
|
588
|
-
(e) => e.partitions.length === partitionCount,
|
|
589
|
-
);
|
|
590
|
-
if (matching.length === 0) {
|
|
591
|
-
throw new Error(
|
|
592
|
-
`Expected rebalance '${type}' for '${destination}' with ${partitionCount} partitions, but none matched`,
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
|
|
598
|
-
getProducerCalls(destination?: string) {
|
|
599
|
-
if (destination) {
|
|
600
|
-
return producerCalls.filter((c) => c.destination === destination);
|
|
601
|
-
}
|
|
602
|
-
return [...producerCalls];
|
|
603
|
-
},
|
|
604
|
-
|
|
605
|
-
getConsumerCalls(destination?: string) {
|
|
606
|
-
if (destination) {
|
|
607
|
-
return consumerCalls.filter((c) => c.destination === destination);
|
|
608
|
-
}
|
|
609
|
-
return [...consumerCalls];
|
|
610
|
-
},
|
|
611
|
-
|
|
612
|
-
getLastProducerCall(destination?: string) {
|
|
613
|
-
const calls = this.getProducerCalls(destination);
|
|
614
|
-
return calls.at(-1);
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
getLastConsumerCall(destination?: string) {
|
|
618
|
-
const calls = this.getConsumerCalls(destination);
|
|
619
|
-
return calls.at(-1);
|
|
620
|
-
},
|
|
621
|
-
|
|
622
|
-
reset() {
|
|
623
|
-
producerCalls.length = 0;
|
|
624
|
-
consumerCalls.length = 0;
|
|
625
|
-
rebalanceEvents.length = 0;
|
|
626
|
-
},
|
|
627
|
-
|
|
628
|
-
shutdown() {
|
|
629
|
-
this.reset();
|
|
630
|
-
},
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// ============================================================================
|
|
635
|
-
// Mock Broker
|
|
636
|
-
// ============================================================================
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Mock message broker for testing
|
|
640
|
-
*/
|
|
641
|
-
export interface MockMessageBroker {
|
|
642
|
-
/** Topics/queues in the broker */
|
|
643
|
-
topics: Map<string, MockMessage[]>;
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Publish a message to a topic
|
|
647
|
-
*/
|
|
648
|
-
publish(topic: string, message: MockMessage): void;
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Consume messages from a topic
|
|
652
|
-
*/
|
|
653
|
-
consume(topic: string, count?: number): MockMessage[];
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Peek at messages without consuming
|
|
657
|
-
*/
|
|
658
|
-
peek(topic: string, count?: number): MockMessage[];
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Get message count for topic
|
|
662
|
-
*/
|
|
663
|
-
getMessageCount(topic: string): number;
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Clear all messages
|
|
667
|
-
*/
|
|
668
|
-
clear(topic?: string): void;
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Create a topic
|
|
672
|
-
*/
|
|
673
|
-
createTopic(topic: string): void;
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Delete a topic
|
|
677
|
-
*/
|
|
678
|
-
deleteTopic(topic: string): void;
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* List all topics
|
|
682
|
-
*/
|
|
683
|
-
listTopics(): string[];
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Create a mock message broker for testing
|
|
688
|
-
*
|
|
689
|
-
* Simulates a message broker (Kafka, SQS, RabbitMQ, etc.) for unit testing.
|
|
690
|
-
*
|
|
691
|
-
* @example
|
|
692
|
-
* ```typescript
|
|
693
|
-
* const broker = createMockMessageBroker();
|
|
694
|
-
*
|
|
695
|
-
* // Producer publishes
|
|
696
|
-
* broker.publish('orders', { payload: { orderId: '123' }, headers: {} });
|
|
697
|
-
*
|
|
698
|
-
* // Consumer receives
|
|
699
|
-
* const messages = broker.consume('orders');
|
|
700
|
-
* expect(messages).toHaveLength(1);
|
|
701
|
-
* expect(messages[0].payload).toEqual({ orderId: '123' });
|
|
702
|
-
* ```
|
|
703
|
-
*/
|
|
704
|
-
export function createMockMessageBroker(): MockMessageBroker {
|
|
705
|
-
const topics = new Map<string, MockMessage[]>();
|
|
706
|
-
|
|
707
|
-
return {
|
|
708
|
-
topics,
|
|
709
|
-
|
|
710
|
-
publish(topic: string, message: MockMessage) {
|
|
711
|
-
if (!topics.has(topic)) {
|
|
712
|
-
topics.set(topic, []);
|
|
713
|
-
}
|
|
714
|
-
topics.get(topic)!.push({
|
|
715
|
-
...message,
|
|
716
|
-
timestamp: message.timestamp ?? Date.now(),
|
|
717
|
-
offset: message.offset ?? topics.get(topic)!.length,
|
|
718
|
-
});
|
|
719
|
-
},
|
|
720
|
-
|
|
721
|
-
consume(topic: string, count?: number) {
|
|
722
|
-
const messages = topics.get(topic) ?? [];
|
|
723
|
-
if (count === undefined) {
|
|
724
|
-
const all = [...messages];
|
|
725
|
-
messages.length = 0;
|
|
726
|
-
return all;
|
|
727
|
-
}
|
|
728
|
-
return messages.splice(0, count);
|
|
729
|
-
},
|
|
730
|
-
|
|
731
|
-
peek(topic: string, count?: number) {
|
|
732
|
-
const messages = topics.get(topic) ?? [];
|
|
733
|
-
if (count === undefined) {
|
|
734
|
-
return [...messages];
|
|
735
|
-
}
|
|
736
|
-
return messages.slice(0, count);
|
|
737
|
-
},
|
|
738
|
-
|
|
739
|
-
getMessageCount(topic: string) {
|
|
740
|
-
return topics.get(topic)?.length ?? 0;
|
|
741
|
-
},
|
|
742
|
-
|
|
743
|
-
clear(topic?: string) {
|
|
744
|
-
if (topic) {
|
|
745
|
-
topics.set(topic, []);
|
|
746
|
-
} else {
|
|
747
|
-
topics.clear();
|
|
748
|
-
}
|
|
749
|
-
},
|
|
750
|
-
|
|
751
|
-
createTopic(topic: string) {
|
|
752
|
-
if (!topics.has(topic)) {
|
|
753
|
-
topics.set(topic, []);
|
|
754
|
-
}
|
|
755
|
-
},
|
|
756
|
-
|
|
757
|
-
deleteTopic(topic: string) {
|
|
758
|
-
topics.delete(topic);
|
|
759
|
-
},
|
|
760
|
-
|
|
761
|
-
listTopics() {
|
|
762
|
-
return [...topics.keys()];
|
|
763
|
-
},
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// ============================================================================
|
|
768
|
-
// Context Propagation Helpers
|
|
769
|
-
// ============================================================================
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Extract trace ID from traceparent header
|
|
773
|
-
*/
|
|
774
|
-
export function extractTraceIdFromHeader(traceparent: string): string | null {
|
|
775
|
-
const parts = traceparent.split('-');
|
|
776
|
-
if (parts.length >= 3 && parts[1] !== undefined) {
|
|
777
|
-
return parts[1];
|
|
778
|
-
}
|
|
779
|
-
return null;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Extract span ID from traceparent header
|
|
784
|
-
*/
|
|
785
|
-
export function extractSpanIdFromHeader(traceparent: string): string | null {
|
|
786
|
-
const parts = traceparent.split('-');
|
|
787
|
-
if (parts.length >= 4 && parts[2] !== undefined) {
|
|
788
|
-
return parts[2];
|
|
789
|
-
}
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Create a mock span context
|
|
795
|
-
*/
|
|
796
|
-
export function createMockSpanContext(
|
|
797
|
-
traceId?: string,
|
|
798
|
-
spanId?: string,
|
|
799
|
-
): SpanContext {
|
|
800
|
-
return {
|
|
801
|
-
traceId: traceId ?? randomHex(32),
|
|
802
|
-
spanId: spanId ?? randomHex(16),
|
|
803
|
-
traceFlags: 1,
|
|
804
|
-
isRemote: true,
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Create a mock link to a producer span
|
|
810
|
-
*/
|
|
811
|
-
export function createMockProducerLink(
|
|
812
|
-
traceId?: string,
|
|
813
|
-
spanId?: string,
|
|
814
|
-
): Link {
|
|
815
|
-
return {
|
|
816
|
-
context: createMockSpanContext(traceId, spanId),
|
|
817
|
-
attributes: {
|
|
818
|
-
'messaging.link.source': 'producer',
|
|
819
|
-
},
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// ============================================================================
|
|
824
|
-
// Scenario Builders
|
|
825
|
-
// ============================================================================
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Create a batch of mock messages
|
|
829
|
-
*/
|
|
830
|
-
export function createMockMessageBatch<T>(
|
|
831
|
-
payloads: T[],
|
|
832
|
-
options: {
|
|
833
|
-
startOffset?: number;
|
|
834
|
-
partition?: number;
|
|
835
|
-
addTraceHeaders?: boolean;
|
|
836
|
-
traceId?: string;
|
|
837
|
-
} = {},
|
|
838
|
-
): MockMessage<T>[] {
|
|
839
|
-
const startOffset = options.startOffset ?? 0;
|
|
840
|
-
const addTraceHeaders = options.addTraceHeaders ?? true;
|
|
841
|
-
const traceId = options.traceId ?? randomHex(32);
|
|
842
|
-
|
|
843
|
-
return payloads.map((payload, index) => ({
|
|
844
|
-
payload,
|
|
845
|
-
headers: addTraceHeaders
|
|
846
|
-
? { traceparent: `00-${traceId}-${randomHex(16)}-01` }
|
|
847
|
-
: undefined,
|
|
848
|
-
offset: startOffset + index,
|
|
849
|
-
partition: options.partition ?? 0,
|
|
850
|
-
messageId: `msg-${randomHex(8)}`,
|
|
851
|
-
timestamp: Date.now() + index,
|
|
852
|
-
}));
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Create a rebalance scenario
|
|
857
|
-
*/
|
|
858
|
-
export function createRebalanceScenario(
|
|
859
|
-
topic: string,
|
|
860
|
-
consumerGroup: string,
|
|
861
|
-
partitions: number[],
|
|
862
|
-
): {
|
|
863
|
-
assignEvent: RecordedRebalanceEvent;
|
|
864
|
-
revokeEvent: RecordedRebalanceEvent;
|
|
865
|
-
} {
|
|
866
|
-
const assignments: PartitionAssignment[] = partitions.map((p) => ({
|
|
867
|
-
topic,
|
|
868
|
-
partition: p,
|
|
869
|
-
offset: 0,
|
|
870
|
-
}));
|
|
871
|
-
|
|
872
|
-
return {
|
|
873
|
-
assignEvent: {
|
|
874
|
-
type: 'assigned',
|
|
875
|
-
partitions: assignments,
|
|
876
|
-
timestamp: Date.now(),
|
|
877
|
-
generation: 1,
|
|
878
|
-
destination: topic,
|
|
879
|
-
consumerGroup,
|
|
880
|
-
},
|
|
881
|
-
revokeEvent: {
|
|
882
|
-
type: 'revoked',
|
|
883
|
-
partitions: assignments,
|
|
884
|
-
timestamp: Date.now() + 1000,
|
|
885
|
-
generation: 2,
|
|
886
|
-
destination: topic,
|
|
887
|
-
consumerGroup,
|
|
888
|
-
},
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* Create an out-of-order scenario
|
|
894
|
-
*/
|
|
895
|
-
export function createOutOfOrderScenario<T>(
|
|
896
|
-
payloads: T[],
|
|
897
|
-
outOfOrderIndices: number[],
|
|
898
|
-
): MockMessage<T>[] {
|
|
899
|
-
const messages = createMockMessageBatch(payloads, { addTraceHeaders: true });
|
|
900
|
-
|
|
901
|
-
// Shuffle specified indices to create out-of-order scenario
|
|
902
|
-
const shuffled = [...messages];
|
|
903
|
-
for (const index of outOfOrderIndices) {
|
|
904
|
-
if (index > 0 && index < shuffled.length) {
|
|
905
|
-
// Swap with previous to create out-of-order
|
|
906
|
-
const prev = shuffled[index - 1]!;
|
|
907
|
-
const curr = shuffled[index]!;
|
|
908
|
-
shuffled[index - 1] = curr;
|
|
909
|
-
shuffled[index] = prev;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return shuffled;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/**
|
|
917
|
-
* Create a duplicate message scenario
|
|
918
|
-
*/
|
|
919
|
-
export function createDuplicateScenario<T>(
|
|
920
|
-
payloads: T[],
|
|
921
|
-
duplicateIndices: number[],
|
|
922
|
-
): MockMessage<T>[] {
|
|
923
|
-
const messages = createMockMessageBatch(payloads, { addTraceHeaders: true });
|
|
924
|
-
const result = [...messages];
|
|
925
|
-
|
|
926
|
-
for (const index of duplicateIndices) {
|
|
927
|
-
const originalMessage = messages[index];
|
|
928
|
-
if (index >= 0 && index < messages.length && originalMessage) {
|
|
929
|
-
// Insert duplicate after the original
|
|
930
|
-
result.splice(index + 1, 0, { ...originalMessage });
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
return result;
|
|
935
|
-
}
|