queen-mq 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +212 -144
  2. package/client-js/client-v2/Queen.js +28 -0
  3. package/client-js/client-v2/README.md +6 -2
  4. package/client-js/client-v2/builders/QueueBuilder.js +7 -1
  5. package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
  6. package/client-js/client-v2/stream/StreamBuilder.js +72 -0
  7. package/client-js/client-v2/stream/StreamConsumer.js +156 -0
  8. package/client-js/client-v2/stream/Window.js +140 -0
  9. package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
  10. package/client-js/test-v2/consume.js +1 -0
  11. package/client-js/test-v2/maintenance.js +261 -0
  12. package/client-js/test-v2/retention.js +69 -0
  13. package/client-js/test-v2/run.js +7 -1
  14. package/package.json +2 -2
  15. package/client-js/client/client.js +0 -1536
  16. package/client-js/client/index.js +0 -4
  17. package/client-js/client/utils/http.js +0 -173
  18. package/client-js/client/utils/loadBalancer.js +0 -152
  19. package/client-js/client/utils/retry.js +0 -41
  20. package/client-js/services/encryptionService.js +0 -82
  21. package/client-js/services/evictionService.js +0 -160
  22. package/client-js/services/retentionService.js +0 -162
  23. package/client-js/services/startupSync.js +0 -35
  24. package/client-js/test/README.md +0 -224
  25. package/client-js/test/advanced-client-tests.js +0 -761
  26. package/client-js/test/advanced-pattern-tests.js +0 -1137
  27. package/client-js/test/bus-mode-tests.js +0 -361
  28. package/client-js/test/core-tests.js +0 -457
  29. package/client-js/test/edge-case-tests.js +0 -562
  30. package/client-js/test/enterprise-tests.js +0 -637
  31. package/client-js/test/human.js +0 -162
  32. package/client-js/test/partition-locking-tests.js +0 -545
  33. package/client-js/test/partition-transaction-tests.js +0 -482
  34. package/client-js/test/qos0-tests.js +0 -334
  35. package/client-js/test/test-new.js +0 -370
  36. package/client-js/test/utils.js +0 -169
  37. package/client-js/test/window-buffer-test.js +0 -114
  38. package/client-js/utils/logger.js +0 -44
  39. package/client-js/utils/uuid.js +0 -5
@@ -1,637 +0,0 @@
1
- /**
2
- * Enterprise Feature Tests for Queen Message Queue
3
- * Tests: Encryption, retention policies, message eviction, combined enterprise features
4
- */
5
-
6
- import { startTest, passTest, failTest, sleep, dbPool, getMessageCount, log } from './utils.js';
7
-
8
- /**
9
- * Test: Message Encryption
10
- */
11
- export async function testMessageEncryption(client) {
12
- startTest('Message Encryption', 'enterprise');
13
-
14
- try {
15
- // Check if encryption is enabled
16
- const encryptionKey = process.env.QUEEN_ENCRYPTION_KEY;
17
- if (!encryptionKey) {
18
- log('Skipping encryption test - QUEEN_ENCRYPTION_KEY not set', 'warning');
19
- passTest('Skipped - encryption key not configured');
20
- return;
21
- }
22
-
23
- const queue = 'test-encryption-queue';
24
-
25
- // Configure queue with encryption
26
- await client.queue(queue, {
27
- encryptionEnabled: true
28
- });
29
-
30
- // Verify encryption is enabled in database
31
- const queueResult = await dbPool.query(`
32
- SELECT encryption_enabled
33
- FROM queen.queues
34
- WHERE name = $1
35
- `, [queue]);
36
-
37
- if (!queueResult.rows[0]?.encryption_enabled) {
38
- throw new Error('Encryption not enabled on queue');
39
- }
40
-
41
- // Push sensitive data
42
- const sensitiveData = {
43
- creditCard: '4111111111111111',
44
- ssn: '123-45-6789',
45
- password: 'super-secret-password',
46
- timestamp: Date.now()
47
- };
48
-
49
- await client.push(queue, sensitiveData);
50
- await sleep(100);
51
-
52
- // Check that the message is encrypted in database
53
- const dbResult = await dbPool.query(`
54
- SELECT m.payload, m.is_encrypted
55
- FROM queen.messages m
56
- JOIN queen.partitions p ON m.partition_id = p.id
57
- JOIN queen.queues q ON p.queue_id = q.id
58
- WHERE q.name = $1
59
- ORDER BY m.created_at DESC
60
- LIMIT 1
61
- `, [queue]);
62
-
63
- if (!dbResult.rows[0]?.is_encrypted) {
64
- throw new Error('Message not marked as encrypted in database');
65
- }
66
-
67
- const storedData = dbResult.rows[0].payload;
68
-
69
- // Verify the stored data is encrypted
70
- if (!storedData.encrypted || !storedData.iv || !storedData.authTag) {
71
- throw new Error('Message not properly encrypted in database');
72
- }
73
-
74
- // Verify the stored data doesn't contain plain text sensitive info
75
- const storedStr = JSON.stringify(storedData);
76
- if (storedStr.includes('4111111111111111') ||
77
- storedStr.includes('123-45-6789') ||
78
- storedStr.includes('super-secret-password')) {
79
- throw new Error('Sensitive data found in plain text - encryption failed!');
80
- }
81
-
82
- // Take the message (should be decrypted automatically)
83
- let decryptedData = null;
84
- for await (const msg of client.take(queue, { limit: 1 })) {
85
- decryptedData = msg.data;
86
- await client.ack(msg);
87
- }
88
-
89
- if (!decryptedData) {
90
- throw new Error('Failed to take encrypted message');
91
- }
92
-
93
- // Verify decrypted data matches original
94
- if (decryptedData.creditCard !== sensitiveData.creditCard ||
95
- decryptedData.ssn !== sensitiveData.ssn ||
96
- decryptedData.password !== sensitiveData.password) {
97
- throw new Error('Decrypted data does not match original');
98
- }
99
-
100
- passTest('Message encryption/decryption works correctly');
101
- } catch (error) {
102
- failTest(error);
103
- }
104
- }
105
-
106
- /**
107
- * Test: Retention Policy - Pending Messages
108
- */
109
- export async function testRetentionPendingMessages(client) {
110
- startTest('Retention Policy - Pending Messages', 'enterprise');
111
-
112
- try {
113
- const queue = 'test-retention-pending';
114
-
115
- // Configure queue with retention for pending messages
116
- await client.queue(queue, {
117
- retentionEnabled: true,
118
- retentionSeconds: 3 // 3 seconds retention for pending messages
119
- });
120
-
121
- // Push messages
122
- await client.push(queue, [
123
- { message: 'Message to be retained', order: 1 },
124
- { message: 'Another message to be retained', order: 2 }
125
- ]);
126
-
127
- // Verify messages are in database
128
- const initialCount = await getMessageCount(queue, 'Default');
129
- if (initialCount !== 2) {
130
- throw new Error(`Expected 2 messages, got ${initialCount}`);
131
- }
132
-
133
- // Wait for retention period
134
- log('Waiting for retention period (3 seconds)...', 'info');
135
- await sleep(3500);
136
-
137
- // Trigger retention by attempting a take
138
- for await (const msg of client.take(queue, { limit: 1 })) {
139
- // Should get nothing or expired messages
140
- }
141
-
142
- // Check if messages were retained
143
- const finalCount = await getMessageCount(queue, 'Default');
144
- log(`Messages after retention period: ${finalCount} (from ${initialCount})`, 'info');
145
-
146
- // Check retention history
147
- const retentionHistory = await dbPool.query(`
148
- SELECT rh.messages_deleted, rh.retention_type
149
- FROM queen.retention_history rh
150
- JOIN queen.partitions p ON rh.partition_id = p.id
151
- JOIN queen.queues q ON p.queue_id = q.id
152
- WHERE q.name = $1
153
- ORDER BY rh.executed_at DESC
154
- LIMIT 1
155
- `, [queue]);
156
-
157
- if (retentionHistory.rows.length > 0) {
158
- log(`Retention history: ${retentionHistory.rows[0].messages_deleted} messages deleted (${retentionHistory.rows[0].retention_type})`, 'info');
159
- }
160
-
161
- passTest('Retention policy for pending messages configured successfully');
162
- } catch (error) {
163
- failTest(error);
164
- }
165
- }
166
-
167
- /**
168
- * Test: Message Eviction
169
- */
170
- export async function testMessageEviction(client) {
171
- startTest('Message Eviction', 'enterprise');
172
-
173
- try {
174
- const queue = 'test-eviction-queue';
175
-
176
- // Configure queue with max wait time
177
- await client.queue(queue, {
178
- maxWaitTimeSeconds: 3
179
- });
180
-
181
- // Push message
182
- await client.push(`${queue}/eviction-partition`, {
183
- message: 'Message to be evicted',
184
- timestamp: Date.now()
185
- });
186
-
187
- // Verify message exists and is pending
188
- const pendingCheck = await dbPool.query(`
189
- SELECT m.id, m.created_at,
190
- pc.last_consumed_id, pc.last_consumed_created_at,
191
- dlq.id as dlq_id
192
- FROM queen.messages m
193
- JOIN queen.partitions p ON m.partition_id = p.id
194
- JOIN queen.queues q ON p.queue_id = q.id
195
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = p.id AND pc.consumer_group = '__QUEUE_MODE__'
196
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id AND dlq.consumer_group = '__QUEUE_MODE__'
197
- WHERE q.name = $1 AND p.name = 'eviction-partition'
198
- ORDER BY m.created_at DESC
199
- LIMIT 1
200
- `, [queue]);
201
-
202
- if (pendingCheck.rows.length === 0) {
203
- throw new Error('Message not found');
204
- }
205
-
206
- // Derive status from cursor position
207
- const msg = pendingCheck.rows[0];
208
- const isDLQ = msg.dlq_id != null;
209
- const isConsumed = msg.last_consumed_id &&
210
- ((msg.created_at < msg.last_consumed_created_at) ||
211
- (msg.created_at.getTime() === msg.last_consumed_created_at?.getTime() && msg.id <= msg.last_consumed_id));
212
- const status = isDLQ ? 'dead_letter' : (isConsumed ? 'completed' : 'pending');
213
-
214
- if (status !== 'pending') {
215
- throw new Error(`Message not in pending status, got: ${status}`);
216
- }
217
-
218
- // Wait for max wait time to expire
219
- log('Waiting for max wait time (3 seconds)...', 'info');
220
- await sleep(3500);
221
-
222
- // Try to take (should trigger eviction)
223
- let gotMessage = false;
224
- for await (const msg of client.take(`${queue}/eviction-partition`, { limit: 1 })) {
225
- gotMessage = true;
226
- }
227
-
228
- // Check if message was evicted (moved to DLQ)
229
- const evictionCheck = await dbPool.query(`
230
- SELECT m.id, dlq.error_message,
231
- CASE WHEN dlq.id IS NOT NULL THEN 'evicted' ELSE 'pending' END as status
232
- FROM queen.messages m
233
- JOIN queen.partitions p ON m.partition_id = p.id
234
- JOIN queen.queues q ON p.queue_id = q.id
235
- LEFT JOIN queen.dead_letter_queue dlq ON dlq.message_id = m.id AND dlq.consumer_group = '__QUEUE_MODE__'
236
- WHERE q.name = $1 AND p.name = 'eviction-partition'
237
- ORDER BY m.created_at DESC
238
- LIMIT 1
239
- `, [queue]);
240
-
241
- if (evictionCheck.rows.length > 0) {
242
- const status = evictionCheck.rows[0].status;
243
- const errorMsg = evictionCheck.rows[0].error_message;
244
-
245
- if (status === 'evicted') {
246
- log(`Message evicted with reason: ${errorMsg}`, 'info');
247
- passTest('Message eviction works correctly');
248
- } else if (!gotMessage) {
249
- log('Message may be evicted by background service', 'info');
250
- passTest('Eviction configured successfully');
251
- } else {
252
- passTest('Eviction configuration accepted');
253
- }
254
- } else {
255
- passTest('Message processed or evicted');
256
- }
257
- } catch (error) {
258
- failTest(error);
259
- }
260
- }
261
-
262
- /**
263
- * Test: Combined Enterprise Features
264
- */
265
- export async function testCombinedEnterpriseFeatures(client) {
266
- startTest('Combined Enterprise Features', 'enterprise');
267
-
268
- try {
269
- const queue = 'test-enterprise-combined';
270
- const encryptionEnabled = !!process.env.QUEEN_ENCRYPTION_KEY;
271
-
272
- // Configure queue with all enterprise features
273
- await client.queue(queue, {
274
- // Core options
275
- leaseTime: 300,
276
- retryLimit: 3,
277
- priority: 10,
278
-
279
- // Enterprise options
280
- encryptionEnabled: encryptionEnabled,
281
- retentionEnabled: true,
282
- retentionSeconds: 60,
283
- completedRetentionSeconds: 30,
284
- partitionRetentionSeconds: 120
285
- });
286
-
287
- // Also set max wait time on the queue
288
- await dbPool.query(`
289
- UPDATE queen.queues
290
- SET max_wait_time_seconds = 45
291
- WHERE name = $1
292
- `, [queue]);
293
-
294
- // Verify all configurations
295
- const configCheck = await dbPool.query(`
296
- SELECT
297
- q.encryption_enabled,
298
- q.max_wait_time_seconds,
299
- q.retention_enabled,
300
- q.retention_seconds,
301
- q.completed_retention_seconds,
302
- q.lease_time,
303
- q.retry_limit,
304
- q.priority
305
- FROM queen.queues q
306
- WHERE q.name = $1
307
- `, [queue]);
308
-
309
- if (configCheck.rows.length === 0) {
310
- throw new Error('Configuration not found');
311
- }
312
-
313
- const config = configCheck.rows[0];
314
-
315
- // Verify enterprise configurations
316
- if (encryptionEnabled && config.encryption_enabled !== encryptionEnabled) {
317
- throw new Error('Encryption not configured correctly');
318
- }
319
-
320
- if (config.max_wait_time_seconds !== 45) {
321
- throw new Error('Max wait time not configured correctly');
322
- }
323
-
324
- if (!config.retention_enabled ||
325
- config.retention_seconds !== 60 ||
326
- config.completed_retention_seconds !== 30) {
327
- throw new Error('Retention options not configured correctly');
328
- }
329
-
330
- // Push a test message
331
- await client.push(queue, {
332
- message: 'Enterprise test message',
333
- sensitive: 'confidential-data',
334
- timestamp: Date.now()
335
- });
336
-
337
- await sleep(100);
338
-
339
- // If encryption is enabled, verify message is encrypted in DB
340
- if (encryptionEnabled) {
341
- const encCheck = await dbPool.query(`
342
- SELECT is_encrypted FROM queen.messages m
343
- JOIN queen.partitions p ON m.partition_id = p.id
344
- JOIN queen.queues q ON p.queue_id = q.id
345
- WHERE q.name = $1
346
- ORDER BY m.created_at DESC
347
- LIMIT 1
348
- `, [queue]);
349
-
350
- if (encCheck.rows[0] && !encCheck.rows[0].is_encrypted) {
351
- throw new Error('Message not encrypted despite encryption being enabled');
352
- }
353
- }
354
-
355
- // Take and complete the message
356
- for await (const msg of client.take(queue, { limit: 1 })) {
357
- await client.ack(msg);
358
- }
359
-
360
- passTest('All enterprise features can be configured together successfully');
361
- } catch (error) {
362
- failTest(error);
363
- }
364
- }
365
-
366
- /**
367
- * Test: Retention Policy - Completed Messages
368
- */
369
- export async function testRetentionCompletedMessages(client) {
370
- startTest('Retention Policy - Completed Messages', 'enterprise');
371
-
372
- try {
373
- const queue = 'test-retention-completed';
374
-
375
- // Configure queue with retention for completed messages
376
- await client.queue(queue, {
377
- retentionEnabled: true,
378
- completedRetentionSeconds: 2 // 2 seconds retention for completed messages
379
- });
380
-
381
- // Push messages
382
- await client.push(queue, { message: 'Message to complete and retain' });
383
- await sleep(100);
384
-
385
- // Take and complete the message
386
- let transactionId;
387
- for await (const msg of client.take(queue, { limit: 1 })) {
388
- transactionId = msg.transactionId;
389
- await client.ack(msg);
390
- }
391
-
392
- if (!transactionId) {
393
- throw new Error('Failed to take message for completion');
394
- }
395
-
396
- // Verify message is marked as completed (cursor-based check using UUIDv7 ordering)
397
- const statusCheck = await dbPool.query(`
398
- SELECT
399
- m.id, m.created_at,
400
- pc.last_consumed_id, pc.last_consumed_created_at,
401
- CASE
402
- WHEN pc.last_consumed_id IS NOT NULL AND m.id <= pc.last_consumed_id
403
- THEN 'completed'
404
- ELSE 'pending'
405
- END as status
406
- FROM queen.messages m
407
- LEFT JOIN queen.partition_consumers pc
408
- ON pc.partition_id = m.partition_id
409
- AND pc.consumer_group = '__QUEUE_MODE__'
410
- WHERE m.transaction_id = $1
411
- `, [transactionId]);
412
-
413
- if (!statusCheck.rows[0] || statusCheck.rows[0].status !== 'completed') {
414
- const row = statusCheck.rows[0];
415
- throw new Error(
416
- `Message not marked as completed.\n` +
417
- ` Status: ${row?.status}\n` +
418
- ` Message ID: ${row?.id}\n` +
419
- ` Cursor ID: ${row?.last_consumed_id}\n` +
420
- ` Has cursor: ${row?.last_consumed_id != null}`
421
- );
422
- }
423
-
424
- // Wait for retention period
425
- log('Waiting for completed retention period (2 seconds)...', 'info');
426
- await sleep(2500);
427
-
428
- // Trigger retention check
429
- for await (const msg of client.take(queue, { limit: 1 })) {
430
- // Should get nothing
431
- }
432
-
433
- // Check if completed message still exists
434
- const retainedCheck = await dbPool.query(`
435
- SELECT m.id,
436
- CASE
437
- WHEN (m.created_at, m.id) <= (pc.last_consumed_created_at, COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
438
- THEN 'completed'
439
- ELSE 'pending'
440
- END as status,
441
- pc.last_consumed_at as completed_at
442
- FROM queen.messages m
443
- LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id AND pc.consumer_group = '__QUEUE_MODE__'
444
- WHERE m.transaction_id = $1
445
- `, [transactionId]);
446
-
447
- const messageExists = retainedCheck.rows.length > 0;
448
- const isCompleted = retainedCheck.rows[0]?.status === 'completed';
449
-
450
- if (!messageExists) {
451
- log('Message was deleted after retention period', 'info');
452
- } else if (isCompleted) {
453
- log('Message retained with completed status', 'info');
454
- }
455
-
456
- passTest('Retention policy for completed messages works correctly');
457
- } catch (error) {
458
- failTest(error);
459
- }
460
- }
461
-
462
- /**
463
- * Test: Partition Retention
464
- */
465
- export async function testPartitionRetention(client) {
466
- startTest('Partition Management', 'enterprise');
467
-
468
- try {
469
- const queue = 'test-partition-management';
470
-
471
- // Configure queue
472
- await client.queue(queue, {
473
- retentionEnabled: false
474
- });
475
-
476
- // Push a message to create Default partition
477
- await client.push(queue, { message: 'Test message' });
478
-
479
- // Verify partition exists
480
- const partitionCheck = await dbPool.query(`
481
- SELECT p.id, p.last_activity
482
- FROM queen.partitions p
483
- JOIN queen.queues q ON p.queue_id = q.id
484
- WHERE q.name = $1 AND p.name = 'Default'
485
- `, [queue]);
486
-
487
- if (partitionCheck.rows.length === 0) {
488
- throw new Error('Default partition not created');
489
- }
490
-
491
- const partitionId = partitionCheck.rows[0].id;
492
- log(`Partition created with ID: ${partitionId}`, 'info');
493
-
494
- // Push another message to update last_activity
495
- await client.push(queue, { message: 'Another message' });
496
-
497
- // Take and complete the messages
498
- for await (const msg of client.take(queue, { limit: 2 })) {
499
- await client.ack(msg);
500
- }
501
-
502
- // Check partition still exists
503
- const stillExists = await dbPool.query(`
504
- SELECT COUNT(*) as count FROM queen.partitions WHERE id = $1
505
- `, [partitionId]);
506
-
507
- if (stillExists.rows[0].count === '0') {
508
- throw new Error('Partition deleted too early');
509
- }
510
-
511
- passTest('Partition management works correctly');
512
- } catch (error) {
513
- failTest(error);
514
- }
515
- }
516
-
517
- /**
518
- * Test: Consumer Encryption/Decryption
519
- */
520
- export async function testConsumerEncryption(client) {
521
- startTest('Consumer Encryption/Decryption', 'enterprise');
522
-
523
- try {
524
- // Check if encryption is enabled
525
- const encryptionKey = process.env.QUEEN_ENCRYPTION_KEY;
526
- if (!encryptionKey) {
527
- log('Skipping consumer encryption test - QUEEN_ENCRYPTION_KEY not set', 'warning');
528
- passTest('Skipped - encryption key not configured');
529
- return;
530
- }
531
-
532
- const queue = 'test-consumer-encryption';
533
-
534
- // Configure queue with encryption
535
- await client.queue(queue, {
536
- encryptionEnabled: true
537
- });
538
-
539
- // Push sensitive data
540
- const sensitiveData = {
541
- creditCard: '4111111111111111',
542
- ssn: '123-45-6789',
543
- apiKey: 'sk_test_12345',
544
- timestamp: Date.now()
545
- };
546
-
547
- await client.push(queue, sensitiveData);
548
- await sleep(100);
549
-
550
- // Take and verify decryption
551
- let decryptedCorrectly = false;
552
- for await (const msg of client.take(queue, { limit: 1 })) {
553
- // Check if data is properly decrypted
554
- if (msg.data && typeof msg.data === 'object') {
555
- if (msg.data.encrypted || msg.data.iv || msg.data.authTag) {
556
- throw new Error('Consumer received encrypted data instead of decrypted!');
557
- }
558
- if (msg.data.creditCard === sensitiveData.creditCard &&
559
- msg.data.ssn === sensitiveData.ssn) {
560
- decryptedCorrectly = true;
561
- }
562
- }
563
- await client.ack(msg);
564
- }
565
-
566
- if (!decryptedCorrectly) {
567
- throw new Error('Decrypted data does not match original');
568
- }
569
-
570
- passTest('Consumer receives properly decrypted messages');
571
- } catch (error) {
572
- failTest(error);
573
- }
574
- }
575
-
576
- /**
577
- * Test: Enterprise Error Handling
578
- */
579
- export async function testEnterpriseErrorHandling(client) {
580
- startTest('Enterprise Error Handling', 'enterprise');
581
-
582
- try {
583
- let errorsCaught = 0;
584
-
585
- // Test 1: Invalid retention seconds
586
- try {
587
- await client.queue('test-enterprise-errors', {
588
- retentionEnabled: true,
589
- retentionSeconds: -10 // Invalid negative value
590
- });
591
- errorsCaught++;
592
- } catch (error) {
593
- errorsCaught++;
594
- }
595
-
596
- // Test 2: Configure encryption without key (if key not set)
597
- if (!process.env.QUEEN_ENCRYPTION_KEY) {
598
- try {
599
- await client.queue('test-enterprise-no-key', {
600
- encryptionEnabled: true
601
- });
602
-
603
- // Try to push encrypted message without key
604
- await client.push('test-enterprise-no-key', {
605
- message: 'Should fail without key'
606
- });
607
-
608
- errorsCaught++;
609
- } catch (error) {
610
- errorsCaught++;
611
- }
612
- } else {
613
- errorsCaught++; // Count as handled
614
- }
615
-
616
- // Test 3: Invalid max wait time
617
- try {
618
- await dbPool.query(`
619
- INSERT INTO queen.queues (name, max_wait_time_seconds)
620
- VALUES ('test-invalid-wait', -100)
621
- ON CONFLICT (name) DO UPDATE SET max_wait_time_seconds = EXCLUDED.max_wait_time_seconds
622
- `);
623
- errorsCaught++;
624
- } catch (error) {
625
- errorsCaught++;
626
- }
627
-
628
- if (errorsCaught >= 2) {
629
- passTest(`Enterprise error handling works (${errorsCaught} scenarios handled)`);
630
- } else {
631
- throw new Error(`Only ${errorsCaught} error scenarios handled`);
632
- }
633
- } catch (error) {
634
- failTest(error);
635
- }
636
- }
637
-