@signaltree/events 7.3.1

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