@signaltree/events 7.3.5 → 7.3.6

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/testing.cjs.js ADDED
@@ -0,0 +1,755 @@
1
+ 'use strict';
2
+
3
+ var factory = require('./factory.cjs.js');
4
+
5
+ /**
6
+ * Mock Event Bus for testing
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const eventBus = createMockEventBus();
11
+ *
12
+ * // Subscribe to events
13
+ * eventBus.subscribe('TradeProposalCreated', (event) => {
14
+ * expect(event.data.tradeId).toBe('123');
15
+ * });
16
+ *
17
+ * // Publish an event
18
+ * await eventBus.publish({
19
+ * type: 'TradeProposalCreated',
20
+ * data: { tradeId: '123' },
21
+ * });
22
+ *
23
+ * // Assert published events
24
+ * expect(eventBus.getPublishedEvents()).toHaveLength(1);
25
+ * expect(eventBus.wasPublished('TradeProposalCreated')).toBe(true);
26
+ * ```
27
+ */
28
+ class MockEventBus {
29
+ options;
30
+ publishedEvents = [];
31
+ subscriptions = new Map();
32
+ allSubscriptions = new Set();
33
+ constructor(options = {}) {
34
+ this.options = options;
35
+ this.options = {
36
+ simulateAsync: false,
37
+ asyncDelayMs: 10,
38
+ autoGenerateIds: true,
39
+ throwOnError: false,
40
+ ...options
41
+ };
42
+ }
43
+ /**
44
+ * Publish an event
45
+ */
46
+ async publish(event, options) {
47
+ // Complete the event
48
+ const fullEvent = {
49
+ ...event,
50
+ id: event.id ?? (this.options.autoGenerateIds ? factory.generateEventId() : 'test-event-id'),
51
+ timestamp: event.timestamp ?? new Date().toISOString(),
52
+ correlationId: event.correlationId ?? factory.generateCorrelationId()
53
+ };
54
+ const queue = options?.queue ?? this.getQueueForPriority(fullEvent.priority);
55
+ // Record the published event
56
+ this.publishedEvents.push({
57
+ event: fullEvent,
58
+ queue,
59
+ publishedAt: new Date(),
60
+ delay: options?.delay
61
+ });
62
+ // Simulate async if configured
63
+ if (this.options.simulateAsync) {
64
+ await this.delay(this.options.asyncDelayMs ?? 10);
65
+ }
66
+ // Notify subscribers
67
+ await this.notifySubscribers(fullEvent);
68
+ return {
69
+ eventId: fullEvent.id,
70
+ queue
71
+ };
72
+ }
73
+ /**
74
+ * Publish multiple events
75
+ */
76
+ async publishBatch(events, options) {
77
+ const correlationId = factory.generateCorrelationId();
78
+ return Promise.all(events.map(event => this.publish({
79
+ ...event,
80
+ correlationId
81
+ }, {
82
+ ...options
83
+ })));
84
+ }
85
+ /**
86
+ * Subscribe to a specific event type
87
+ */
88
+ subscribe(eventType, handler) {
89
+ let handlers = this.subscriptions.get(eventType);
90
+ if (!handlers) {
91
+ handlers = new Set();
92
+ this.subscriptions.set(eventType, handlers);
93
+ }
94
+ handlers.add(handler);
95
+ // Return unsubscribe function
96
+ // handlers is guaranteed to exist at this point since we just set it above
97
+ return () => {
98
+ const h = this.subscriptions.get(eventType);
99
+ if (h) {
100
+ h.delete(handler);
101
+ }
102
+ };
103
+ }
104
+ /**
105
+ * Subscribe to all events
106
+ */
107
+ subscribeAll(handler) {
108
+ this.allSubscriptions.add(handler);
109
+ return () => {
110
+ this.allSubscriptions.delete(handler);
111
+ };
112
+ }
113
+ /**
114
+ * Get all published events
115
+ */
116
+ getPublishedEvents() {
117
+ return [...this.publishedEvents];
118
+ }
119
+ /**
120
+ * Get published events by type
121
+ */
122
+ getPublishedEventsByType(type) {
123
+ return this.publishedEvents.filter(p => p.event.type === type);
124
+ }
125
+ /**
126
+ * Get the last published event
127
+ */
128
+ getLastPublishedEvent() {
129
+ return this.publishedEvents[this.publishedEvents.length - 1];
130
+ }
131
+ /**
132
+ * Get the last published event of a specific type
133
+ */
134
+ getLastPublishedEventByType(type) {
135
+ const events = this.getPublishedEventsByType(type);
136
+ return events[events.length - 1];
137
+ }
138
+ /**
139
+ * Check if an event type was published
140
+ */
141
+ wasPublished(eventType) {
142
+ return this.publishedEvents.some(p => p.event.type === eventType);
143
+ }
144
+ /**
145
+ * Check if an event with specific data was published
146
+ */
147
+ wasPublishedWith(eventType, predicate) {
148
+ return this.publishedEvents.some(p => p.event.type === eventType && predicate(p.event));
149
+ }
150
+ /**
151
+ * Get count of published events
152
+ */
153
+ getPublishedCount(eventType) {
154
+ if (eventType) {
155
+ return this.publishedEvents.filter(p => p.event.type === eventType).length;
156
+ }
157
+ return this.publishedEvents.length;
158
+ }
159
+ /**
160
+ * Clear all published events (for test cleanup)
161
+ */
162
+ clearHistory() {
163
+ this.publishedEvents = [];
164
+ }
165
+ /**
166
+ * Clear all subscriptions
167
+ */
168
+ clearSubscriptions() {
169
+ this.subscriptions.clear();
170
+ this.allSubscriptions.clear();
171
+ }
172
+ /**
173
+ * Reset the mock (clear history and subscriptions)
174
+ */
175
+ reset() {
176
+ this.clearHistory();
177
+ this.clearSubscriptions();
178
+ }
179
+ /**
180
+ * Simulate an incoming event (as if received from server)
181
+ */
182
+ async simulateIncomingEvent(event) {
183
+ await this.notifySubscribers(event);
184
+ }
185
+ // Private methods
186
+ async notifySubscribers(event) {
187
+ const errors = [];
188
+ // Notify type-specific subscribers
189
+ const handlers = this.subscriptions.get(event.type);
190
+ if (handlers) {
191
+ for (const handler of handlers) {
192
+ try {
193
+ await handler(event);
194
+ } catch (error) {
195
+ errors.push(error instanceof Error ? error : new Error(String(error)));
196
+ }
197
+ }
198
+ }
199
+ // Notify all-event subscribers
200
+ for (const handler of this.allSubscriptions) {
201
+ try {
202
+ await handler(event);
203
+ } catch (error) {
204
+ errors.push(error instanceof Error ? error : new Error(String(error)));
205
+ }
206
+ }
207
+ if (errors.length > 0 && this.options.throwOnError) {
208
+ throw new AggregateError(errors, 'Subscriber errors');
209
+ }
210
+ }
211
+ getQueueForPriority(priority) {
212
+ switch (priority) {
213
+ case 'critical':
214
+ return 'events-critical';
215
+ case 'high':
216
+ return 'events-high';
217
+ case 'low':
218
+ return 'events-low';
219
+ case 'bulk':
220
+ return 'events-bulk';
221
+ default:
222
+ return 'events-normal';
223
+ }
224
+ }
225
+ delay(ms) {
226
+ return new Promise(resolve => setTimeout(resolve, ms));
227
+ }
228
+ }
229
+ /**
230
+ * Create a mock event bus for testing
231
+ */
232
+ function createMockEventBus(options) {
233
+ return new MockEventBus(options);
234
+ }
235
+
236
+ /**
237
+ * Default test actor
238
+ */
239
+ const DEFAULT_TEST_ACTOR = {
240
+ id: 'test-user-1',
241
+ type: 'user',
242
+ name: 'Test User'
243
+ };
244
+ /**
245
+ * Default test metadata
246
+ */
247
+ const DEFAULT_TEST_METADATA = {
248
+ source: 'test',
249
+ environment: 'test'
250
+ };
251
+ /**
252
+ * Create a test event
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const event = createTestEvent('TradeProposalCreated', {
257
+ * tradeId: '123',
258
+ * initiatorId: 'user-1',
259
+ * recipientId: 'user-2',
260
+ * });
261
+ * ```
262
+ */
263
+ function createTestEvent(type, data, options = {}) {
264
+ return {
265
+ id: options.id ?? factory.generateEventId(),
266
+ type,
267
+ version: options.version ?? factory.DEFAULT_EVENT_VERSION,
268
+ timestamp: options.timestamp ?? new Date().toISOString(),
269
+ correlationId: options.correlationId ?? factory.generateCorrelationId(),
270
+ causationId: options.causationId,
271
+ actor: {
272
+ ...DEFAULT_TEST_ACTOR,
273
+ ...options.actor
274
+ },
275
+ metadata: {
276
+ ...DEFAULT_TEST_METADATA,
277
+ ...options.metadata
278
+ },
279
+ data,
280
+ priority: options.priority,
281
+ aggregate: options.aggregate
282
+ };
283
+ }
284
+ /**
285
+ * Create a test event factory
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * const factory = createTestEventFactory({
290
+ * defaultActor: { id: 'test-user', type: 'user' },
291
+ * defaultMetadata: { source: 'test-service' },
292
+ * });
293
+ *
294
+ * const event = factory.create('TradeProposalCreated', {
295
+ * tradeId: '123',
296
+ * });
297
+ * ```
298
+ */
299
+ function createTestEventFactory(config) {
300
+ const defaultActor = {
301
+ ...DEFAULT_TEST_ACTOR,
302
+ ...config?.defaultActor
303
+ };
304
+ const defaultMetadata = {
305
+ ...DEFAULT_TEST_METADATA,
306
+ ...config?.defaultMetadata
307
+ };
308
+ return {
309
+ create(type, data, options = {}) {
310
+ return {
311
+ id: options.id ?? factory.generateEventId(),
312
+ type,
313
+ version: options.version ?? factory.DEFAULT_EVENT_VERSION,
314
+ timestamp: options.timestamp ?? new Date().toISOString(),
315
+ correlationId: options.correlationId ?? factory.generateCorrelationId(),
316
+ causationId: options.causationId,
317
+ actor: {
318
+ ...defaultActor,
319
+ ...options.actor
320
+ },
321
+ metadata: {
322
+ ...defaultMetadata,
323
+ ...options.metadata
324
+ },
325
+ data,
326
+ priority: options.priority,
327
+ aggregate: options.aggregate
328
+ };
329
+ },
330
+ createMany(type, dataArray, options = {}) {
331
+ const correlationId = options.correlationId ?? factory.generateCorrelationId();
332
+ return dataArray.map((data, index) => this.create(type, data, {
333
+ ...options,
334
+ correlationId,
335
+ causationId: index > 0 ? undefined : options.causationId
336
+ }));
337
+ },
338
+ createRandom(type, overrides = {}, options = {}) {
339
+ // Get random generator for this type if available
340
+ const generator = config?.randomGenerators?.[type];
341
+ const randomData = generator ? generator() : {};
342
+ const overrideData = overrides ? overrides : {};
343
+ return this.create(type, {
344
+ ...randomData,
345
+ ...overrideData
346
+ }, options);
347
+ }
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Event assertions class
353
+ */
354
+ class EventAssertions {
355
+ publishedEvents;
356
+ constructor(events) {
357
+ this.publishedEvents = events;
358
+ }
359
+ /**
360
+ * Assert that an event was published
361
+ */
362
+ toHavePublished(eventType) {
363
+ const found = this.publishedEvents.some(p => p.event.type === eventType);
364
+ return {
365
+ passed: found,
366
+ message: found ? `Event "${eventType}" was published` : `Expected event "${eventType}" to be published, but it was not`,
367
+ expected: eventType,
368
+ actual: this.publishedEvents.map(p => p.event.type)
369
+ };
370
+ }
371
+ /**
372
+ * Assert that an event was NOT published
373
+ */
374
+ toNotHavePublished(eventType) {
375
+ const found = this.publishedEvents.some(p => p.event.type === eventType);
376
+ return {
377
+ passed: !found,
378
+ message: !found ? `Event "${eventType}" was not published` : `Expected event "${eventType}" to NOT be published, but it was`,
379
+ expected: `NOT ${eventType}`,
380
+ actual: this.publishedEvents.map(p => p.event.type)
381
+ };
382
+ }
383
+ /**
384
+ * Assert the number of published events
385
+ */
386
+ toHavePublishedCount(count, eventType) {
387
+ const events = eventType ? this.publishedEvents.filter(p => p.event.type === eventType) : this.publishedEvents;
388
+ const actual = events.length;
389
+ const passed = actual === count;
390
+ return {
391
+ passed,
392
+ message: passed ? `Published ${count} events${eventType ? ` of type "${eventType}"` : ''}` : `Expected ${count} events${eventType ? ` of type "${eventType}"` : ''}, got ${actual}`,
393
+ expected: count,
394
+ actual
395
+ };
396
+ }
397
+ /**
398
+ * Assert event was published with specific data
399
+ */
400
+ toHavePublishedWith(eventType, predicate) {
401
+ const matching = this.publishedEvents.find(p => p.event.type === eventType && predicate(p.event));
402
+ return {
403
+ passed: !!matching,
404
+ message: matching ? `Found event "${eventType}" matching predicate` : `Expected to find event "${eventType}" matching predicate`,
405
+ expected: eventType,
406
+ actual: matching?.event
407
+ };
408
+ }
409
+ /**
410
+ * Assert events were published in order
411
+ */
412
+ toHavePublishedInOrder(eventTypes) {
413
+ const publishedTypes = this.publishedEvents.map(p => p.event.type);
414
+ let typeIndex = 0;
415
+ for (const publishedType of publishedTypes) {
416
+ if (publishedType === eventTypes[typeIndex]) {
417
+ typeIndex++;
418
+ if (typeIndex === eventTypes.length) break;
419
+ }
420
+ }
421
+ const passed = typeIndex === eventTypes.length;
422
+ return {
423
+ passed,
424
+ message: passed ? `Events published in expected order` : `Expected events in order: [${eventTypes.join(', ')}], got: [${publishedTypes.join(', ')}]`,
425
+ expected: eventTypes,
426
+ actual: publishedTypes
427
+ };
428
+ }
429
+ /**
430
+ * Assert event has valid structure
431
+ */
432
+ toBeValidEvent(event) {
433
+ const issues = [];
434
+ if (!event.id) issues.push('Missing id');
435
+ if (!event.type) issues.push('Missing type');
436
+ if (!event.timestamp) issues.push('Missing timestamp');
437
+ if (!event.correlationId) issues.push('Missing correlationId');
438
+ if (!event.version) issues.push('Missing version');
439
+ if (!event.actor) issues.push('Missing actor');
440
+ if (!event.metadata) issues.push('Missing metadata');
441
+ if (event.actor) {
442
+ if (!event.actor.id) issues.push('Missing actor.id');
443
+ if (!event.actor.type) issues.push('Missing actor.type');
444
+ }
445
+ if (event.metadata) {
446
+ if (!event.metadata.source) issues.push('Missing metadata.source');
447
+ }
448
+ const passed = issues.length === 0;
449
+ return {
450
+ passed,
451
+ message: passed ? 'Event has valid structure' : `Event has invalid structure: ${issues.join(', ')}`,
452
+ expected: 'Valid event structure',
453
+ actual: issues
454
+ };
455
+ }
456
+ /**
457
+ * Assert all events have same correlation ID
458
+ */
459
+ toHaveSameCorrelationId() {
460
+ if (this.publishedEvents.length === 0) {
461
+ return {
462
+ passed: true,
463
+ message: 'No events to check'
464
+ };
465
+ }
466
+ const correlationId = this.publishedEvents[0].event.correlationId;
467
+ const allMatch = this.publishedEvents.every(p => p.event.correlationId === correlationId);
468
+ return {
469
+ passed: allMatch,
470
+ message: allMatch ? `All events have correlation ID: ${correlationId}` : 'Events have different correlation IDs',
471
+ expected: correlationId,
472
+ actual: this.publishedEvents.map(p => p.event.correlationId)
473
+ };
474
+ }
475
+ /**
476
+ * Assert event was published to specific queue
477
+ */
478
+ toHavePublishedToQueue(eventType, queue) {
479
+ const matching = this.publishedEvents.find(p => p.event.type === eventType && p.queue === queue);
480
+ return {
481
+ passed: !!matching,
482
+ message: matching ? `Event "${eventType}" was published to queue "${queue}"` : `Expected event "${eventType}" to be published to queue "${queue}"`,
483
+ expected: {
484
+ eventType,
485
+ queue
486
+ },
487
+ actual: this.publishedEvents.filter(p => p.event.type === eventType).map(p => ({
488
+ type: p.event.type,
489
+ queue: p.queue
490
+ }))
491
+ };
492
+ }
493
+ /**
494
+ * Get all assertions as an array
495
+ */
496
+ getAllResults() {
497
+ return [];
498
+ }
499
+ }
500
+ /**
501
+ * Create event assertions helper
502
+ *
503
+ * @example
504
+ * ```typescript
505
+ * const eventBus = createMockEventBus();
506
+ * // ... publish events ...
507
+ *
508
+ * const assertions = createEventAssertions(eventBus.getPublishedEvents());
509
+ *
510
+ * expect(assertions.toHavePublished('TradeCreated').passed).toBe(true);
511
+ * expect(assertions.toHavePublishedCount(3).passed).toBe(true);
512
+ * expect(assertions.toHavePublishedInOrder(['TradeCreated', 'TradeAccepted']).passed).toBe(true);
513
+ * ```
514
+ */
515
+ function createEventAssertions(events) {
516
+ return new EventAssertions(events);
517
+ }
518
+
519
+ /**
520
+ * Test Helpers - Utility functions for testing
521
+ *
522
+ * Provides:
523
+ * - Event waiting utilities
524
+ * - Mock implementations
525
+ * - Test fixtures
526
+ */
527
+ /**
528
+ * Wait for an event to be published
529
+ *
530
+ * @example
531
+ * ```typescript
532
+ * const eventPromise = waitForEvent(eventBus, 'TradeCreated');
533
+ * await performAction();
534
+ * const event = await eventPromise;
535
+ * expect(event.data.tradeId).toBe('123');
536
+ * ```
537
+ */
538
+ function waitForEvent(eventBus, eventType, timeoutMs = 5000) {
539
+ return new Promise((resolve, reject) => {
540
+ const timeout = setTimeout(() => {
541
+ unsubscribe();
542
+ reject(new Error(`Timeout waiting for event "${eventType}" after ${timeoutMs}ms`));
543
+ }, timeoutMs);
544
+ const unsubscribe = eventBus.subscribe(eventType, event => {
545
+ clearTimeout(timeout);
546
+ unsubscribe();
547
+ resolve(event);
548
+ });
549
+ });
550
+ }
551
+ /**
552
+ * Wait for multiple events to be published
553
+ *
554
+ * @example
555
+ * ```typescript
556
+ * const eventsPromise = waitForEvents(eventBus, ['TradeCreated', 'NotificationSent']);
557
+ * await performAction();
558
+ * const events = await eventsPromise;
559
+ * ```
560
+ */
561
+ function waitForEvents(eventBus, eventTypes, timeoutMs = 5000) {
562
+ return new Promise((resolve, reject) => {
563
+ const remaining = new Set(eventTypes);
564
+ const collected = [];
565
+ const unsubscribes = [];
566
+ const timeout = setTimeout(() => {
567
+ unsubscribes.forEach(unsub => unsub());
568
+ reject(new Error(`Timeout waiting for events: [${Array.from(remaining).join(', ')}] after ${timeoutMs}ms`));
569
+ }, timeoutMs);
570
+ const checkComplete = () => {
571
+ if (remaining.size === 0) {
572
+ clearTimeout(timeout);
573
+ unsubscribes.forEach(unsub => unsub());
574
+ resolve(collected);
575
+ }
576
+ };
577
+ for (const eventType of eventTypes) {
578
+ const unsub = eventBus.subscribe(eventType, event => {
579
+ if (remaining.has(eventType)) {
580
+ remaining.delete(eventType);
581
+ collected.push(event);
582
+ checkComplete();
583
+ }
584
+ });
585
+ unsubscribes.push(unsub);
586
+ }
587
+ });
588
+ }
589
+ /**
590
+ * Collect events during an action
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * const events = await collectEvents(eventBus, async () => {
595
+ * await createTrade();
596
+ * await acceptTrade();
597
+ * });
598
+ * expect(events).toHaveLength(2);
599
+ * ```
600
+ */
601
+ async function collectEvents(eventBus, action, eventTypes) {
602
+ const events = [];
603
+ const unsubscribe = eventBus.subscribeAll(event => {
604
+ if (!eventTypes || eventTypes.includes(event.type)) {
605
+ events.push(event);
606
+ }
607
+ });
608
+ try {
609
+ await action();
610
+ // Give async handlers time to complete
611
+ await new Promise(resolve => setTimeout(resolve, 10));
612
+ } finally {
613
+ unsubscribe();
614
+ }
615
+ return events;
616
+ }
617
+ /**
618
+ * Create a mock idempotency store
619
+ *
620
+ * @example
621
+ * ```typescript
622
+ * const store = mockIdempotencyStore({
623
+ * duplicateIds: ['event-1', 'event-2'],
624
+ * });
625
+ *
626
+ * const result = await store.check({ id: 'event-1' }, 'consumer');
627
+ * expect(result.isDuplicate).toBe(true);
628
+ * ```
629
+ */
630
+ function mockIdempotencyStore(options) {
631
+ const duplicateIds = new Set(options?.duplicateIds ?? []);
632
+ const processedRecords = options?.processedRecords ?? new Map();
633
+ const processingLocks = new Map();
634
+ return {
635
+ async check(event, consumer, checkOptions) {
636
+ const key = `${consumer}:${event.id}`;
637
+ if (duplicateIds.has(event.id) || processedRecords.has(key)) {
638
+ const record = processedRecords.get(key);
639
+ return {
640
+ isDuplicate: true,
641
+ processedAt: record?.completedAt ?? record?.startedAt,
642
+ result: record?.result
643
+ };
644
+ }
645
+ const shouldAcquire = options?.shouldAcquireLock ?? checkOptions?.acquireLock ?? true;
646
+ if (shouldAcquire) {
647
+ processingLocks.set(key, true);
648
+ }
649
+ return {
650
+ isDuplicate: false,
651
+ lockAcquired: shouldAcquire
652
+ };
653
+ },
654
+ async markProcessing(event, consumer) {
655
+ const key = `${consumer}:${event.id}`;
656
+ if (processingLocks.has(key)) {
657
+ return false;
658
+ }
659
+ processingLocks.set(key, true);
660
+ return true;
661
+ },
662
+ async markCompleted(event, consumer, result) {
663
+ const key = `${consumer}:${event.id}`;
664
+ processedRecords.set(key, {
665
+ eventId: event.id,
666
+ eventType: event.type,
667
+ startedAt: new Date(),
668
+ completedAt: new Date(),
669
+ status: 'completed',
670
+ result,
671
+ consumer,
672
+ attempts: 1
673
+ });
674
+ processingLocks.delete(key);
675
+ },
676
+ async markFailed(event, consumer, error) {
677
+ const key = `${consumer}:${event.id}`;
678
+ processedRecords.set(key, {
679
+ eventId: event.id,
680
+ eventType: event.type,
681
+ startedAt: new Date(),
682
+ completedAt: new Date(),
683
+ status: 'failed',
684
+ error: error instanceof Error ? error.message : String(error),
685
+ consumer,
686
+ attempts: 1
687
+ });
688
+ processingLocks.delete(key);
689
+ },
690
+ async releaseLock(event, consumer) {
691
+ const key = `${consumer}:${event.id}`;
692
+ processingLocks.delete(key);
693
+ },
694
+ async getRecord(eventId, consumer) {
695
+ return processedRecords.get(`${consumer}:${eventId}`) ?? null;
696
+ }
697
+ };
698
+ }
699
+ /**
700
+ * Create a mock error classifier
701
+ *
702
+ * @example
703
+ * ```typescript
704
+ * const classifier = mockErrorClassifier({
705
+ * defaultClassification: 'transient',
706
+ * customClassifications: {
707
+ * 'ValidationError': 'permanent',
708
+ * 'NetworkError': 'transient',
709
+ * },
710
+ * });
711
+ * ```
712
+ */
713
+ function mockErrorClassifier(options) {
714
+ const defaultClassification = options?.defaultClassification ?? 'unknown';
715
+ const customClassifications = options?.customClassifications ?? {};
716
+ const defaultRetryConfig = {
717
+ maxAttempts: 3,
718
+ initialDelayMs: 1000,
719
+ maxDelayMs: 30000,
720
+ backoffMultiplier: 2,
721
+ jitter: 0.1,
722
+ ...options?.retryConfig
723
+ };
724
+ return {
725
+ classify(error) {
726
+ const errorName = error instanceof Error ? error.constructor.name : 'Error';
727
+ const errorMessage = error instanceof Error ? error.message : String(error);
728
+ // Check custom classifications
729
+ const customClass = customClassifications[errorName] ?? customClassifications[errorMessage];
730
+ const classification = customClass ?? defaultClassification;
731
+ return {
732
+ classification,
733
+ retryConfig: defaultRetryConfig,
734
+ sendToDlq: classification === 'permanent' || classification === 'poison',
735
+ reason: `Mock classification: ${classification}`
736
+ };
737
+ },
738
+ isRetryable(error) {
739
+ const result = this.classify(error);
740
+ return result.classification === 'transient' || result.classification === 'unknown';
741
+ }
742
+ };
743
+ }
744
+
745
+ exports.EventAssertions = EventAssertions;
746
+ exports.MockEventBus = MockEventBus;
747
+ exports.collectEvents = collectEvents;
748
+ exports.createEventAssertions = createEventAssertions;
749
+ exports.createMockEventBus = createMockEventBus;
750
+ exports.createTestEvent = createTestEvent;
751
+ exports.createTestEventFactory = createTestEventFactory;
752
+ exports.mockErrorClassifier = mockErrorClassifier;
753
+ exports.mockIdempotencyStore = mockIdempotencyStore;
754
+ exports.waitForEvent = waitForEvent;
755
+ exports.waitForEvents = waitForEvents;