@cdklabs/cdk-appmod-catalog-blueprints 1.5.0 → 1.6.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 (92) hide show
  1. package/.jsii +2537 -204
  2. package/lib/document-processing/adapter/adapter.d.ts +4 -2
  3. package/lib/document-processing/adapter/adapter.js +1 -1
  4. package/lib/document-processing/adapter/queued-s3-adapter.d.ts +9 -2
  5. package/lib/document-processing/adapter/queued-s3-adapter.js +29 -15
  6. package/lib/document-processing/agentic-document-processing.d.ts +4 -0
  7. package/lib/document-processing/agentic-document-processing.js +20 -10
  8. package/lib/document-processing/base-document-processing.d.ts +54 -2
  9. package/lib/document-processing/base-document-processing.js +136 -82
  10. package/lib/document-processing/bedrock-document-processing.d.ts +202 -2
  11. package/lib/document-processing/bedrock-document-processing.js +717 -77
  12. package/lib/document-processing/chunking-config.d.ts +614 -0
  13. package/lib/document-processing/chunking-config.js +5 -0
  14. package/lib/document-processing/default-document-processing-config.js +1 -1
  15. package/lib/document-processing/index.d.ts +1 -0
  16. package/lib/document-processing/index.js +2 -1
  17. package/lib/document-processing/resources/aggregation/handler.py +567 -0
  18. package/lib/document-processing/resources/aggregation/requirements.txt +7 -0
  19. package/lib/document-processing/resources/aggregation/test_handler.py +362 -0
  20. package/lib/document-processing/resources/cleanup/handler.py +276 -0
  21. package/lib/document-processing/resources/cleanup/requirements.txt +5 -0
  22. package/lib/document-processing/resources/cleanup/test_handler.py +436 -0
  23. package/lib/document-processing/resources/default-bedrock-invoke/index.py +85 -3
  24. package/lib/document-processing/resources/default-bedrock-invoke/test_index.py +622 -0
  25. package/lib/document-processing/resources/pdf-chunking/README.md +313 -0
  26. package/lib/document-processing/resources/pdf-chunking/chunking_strategies.py +460 -0
  27. package/lib/document-processing/resources/pdf-chunking/error_handling.py +491 -0
  28. package/lib/document-processing/resources/pdf-chunking/handler.py +958 -0
  29. package/lib/document-processing/resources/pdf-chunking/metrics.py +435 -0
  30. package/lib/document-processing/resources/pdf-chunking/requirements.txt +3 -0
  31. package/lib/document-processing/resources/pdf-chunking/strategy_selection.py +420 -0
  32. package/lib/document-processing/resources/pdf-chunking/structured_logging.py +457 -0
  33. package/lib/document-processing/resources/pdf-chunking/test_chunking_strategies.py +353 -0
  34. package/lib/document-processing/resources/pdf-chunking/test_error_handling.py +487 -0
  35. package/lib/document-processing/resources/pdf-chunking/test_handler.py +609 -0
  36. package/lib/document-processing/resources/pdf-chunking/test_integration.py +694 -0
  37. package/lib/document-processing/resources/pdf-chunking/test_metrics.py +532 -0
  38. package/lib/document-processing/resources/pdf-chunking/test_strategy_selection.py +471 -0
  39. package/lib/document-processing/resources/pdf-chunking/test_structured_logging.py +449 -0
  40. package/lib/document-processing/resources/pdf-chunking/test_token_estimation.py +374 -0
  41. package/lib/document-processing/resources/pdf-chunking/token_estimation.py +189 -0
  42. package/lib/document-processing/tests/agentic-document-processing-nag.test.js +4 -3
  43. package/lib/document-processing/tests/agentic-document-processing.test.js +488 -4
  44. package/lib/document-processing/tests/base-document-processing-nag.test.js +9 -2
  45. package/lib/document-processing/tests/base-document-processing-schema.test.d.ts +1 -0
  46. package/lib/document-processing/tests/base-document-processing-schema.test.js +337 -0
  47. package/lib/document-processing/tests/base-document-processing.test.js +114 -8
  48. package/lib/document-processing/tests/bedrock-document-processing-chunking-nag.test.d.ts +1 -0
  49. package/lib/document-processing/tests/bedrock-document-processing-chunking-nag.test.js +382 -0
  50. package/lib/document-processing/tests/bedrock-document-processing-nag.test.js +4 -3
  51. package/lib/document-processing/tests/bedrock-document-processing-security.test.d.ts +1 -0
  52. package/lib/document-processing/tests/bedrock-document-processing-security.test.js +389 -0
  53. package/lib/document-processing/tests/bedrock-document-processing.test.js +808 -8
  54. package/lib/document-processing/tests/chunking-config.test.d.ts +1 -0
  55. package/lib/document-processing/tests/chunking-config.test.js +238 -0
  56. package/lib/document-processing/tests/queued-s3-adapter-nag.test.js +9 -2
  57. package/lib/document-processing/tests/queued-s3-adapter.test.js +17 -6
  58. package/lib/framework/agents/base-agent.js +1 -1
  59. package/lib/framework/agents/batch-agent.js +1 -1
  60. package/lib/framework/agents/default-agent-config.js +1 -1
  61. package/lib/framework/bedrock/bedrock.js +1 -1
  62. package/lib/framework/custom-resource/default-runtimes.js +1 -1
  63. package/lib/framework/foundation/access-log.js +1 -1
  64. package/lib/framework/foundation/eventbridge-broker.js +1 -1
  65. package/lib/framework/foundation/network.js +1 -1
  66. package/lib/framework/tests/access-log.test.js +5 -2
  67. package/lib/framework/tests/batch-agent.test.js +5 -2
  68. package/lib/framework/tests/bedrock.test.js +5 -2
  69. package/lib/framework/tests/eventbridge-broker.test.js +5 -2
  70. package/lib/framework/tests/framework-nag.test.js +16 -8
  71. package/lib/framework/tests/network.test.js +9 -4
  72. package/lib/tsconfig.tsbuildinfo +1 -1
  73. package/lib/utilities/data-loader.js +1 -1
  74. package/lib/utilities/lambda-iam-utils.js +1 -1
  75. package/lib/utilities/observability/cloudfront-distribution-observability-property-injector.js +1 -1
  76. package/lib/utilities/observability/default-observability-config.js +1 -1
  77. package/lib/utilities/observability/lambda-observability-property-injector.js +1 -1
  78. package/lib/utilities/observability/log-group-data-protection-utils.js +1 -1
  79. package/lib/utilities/observability/powertools-config.d.ts +10 -1
  80. package/lib/utilities/observability/powertools-config.js +19 -3
  81. package/lib/utilities/observability/state-machine-observability-property-injector.js +1 -1
  82. package/lib/utilities/test-utils.d.ts +43 -0
  83. package/lib/utilities/test-utils.js +56 -0
  84. package/lib/utilities/tests/data-loader-nag.test.js +3 -2
  85. package/lib/utilities/tests/data-loader.test.js +3 -2
  86. package/lib/webapp/frontend-construct.js +1 -1
  87. package/lib/webapp/tests/frontend-construct-nag.test.js +3 -2
  88. package/lib/webapp/tests/frontend-construct.test.js +3 -2
  89. package/package.json +6 -5
  90. package/lib/document-processing/resources/default-error-handler/index.js +0 -46
  91. package/lib/document-processing/resources/default-pdf-processor/index.js +0 -46
  92. package/lib/document-processing/resources/default-pdf-validator/index.js +0 -36
@@ -0,0 +1,487 @@
1
+ """
2
+ Unit tests for error handling module.
3
+
4
+ Tests cover:
5
+ - Invalid PDF format handling
6
+ - Corrupted PDF handling
7
+ - S3 access denied handling
8
+ - Lambda timeout handling
9
+ - DynamoDB write failure handling
10
+ - Retry logic with exponential backoff
11
+
12
+ Requirements: 7.1, 7.2, 7.3
13
+ """
14
+
15
+ import unittest
16
+ from unittest.mock import Mock, patch, MagicMock
17
+ import time
18
+ from botocore.exceptions import ClientError
19
+
20
+ from error_handling import (
21
+ PDFChunkingError,
22
+ InvalidPDFFormatError,
23
+ CorruptedPDFError,
24
+ EncryptedPDFError,
25
+ S3AccessDeniedError,
26
+ S3NotFoundError,
27
+ S3ThrottlingError,
28
+ DynamoDBWriteError,
29
+ ChunkingTimeoutError,
30
+ ConfigurationError,
31
+ classify_s3_error,
32
+ classify_pdf_error,
33
+ create_error_response,
34
+ log_error,
35
+ retry_with_exponential_backoff,
36
+ validate_pdf_magic_bytes
37
+ )
38
+
39
+
40
+ class TestPDFChunkingError(unittest.TestCase):
41
+ """Test cases for base PDFChunkingError class."""
42
+
43
+ def test_error_creation(self):
44
+ """Test creating a PDFChunkingError."""
45
+ error = PDFChunkingError(
46
+ message="Test error",
47
+ error_type="TestError",
48
+ document_id="doc-123",
49
+ recoverable=True,
50
+ details={'key': 'value'}
51
+ )
52
+
53
+ self.assertEqual(error.message, "Test error")
54
+ self.assertEqual(error.error_type, "TestError")
55
+ self.assertEqual(error.document_id, "doc-123")
56
+ self.assertTrue(error.recoverable)
57
+ self.assertEqual(error.details, {'key': 'value'})
58
+
59
+ def test_error_str(self):
60
+ """Test error string representation."""
61
+ error = PDFChunkingError(
62
+ message="Test error message",
63
+ error_type="TestError"
64
+ )
65
+
66
+ self.assertEqual(str(error), "Test error message")
67
+
68
+
69
+ class TestInvalidPDFFormatError(unittest.TestCase):
70
+ """Test cases for InvalidPDFFormatError."""
71
+
72
+ def test_error_type(self):
73
+ """Test that error type is set correctly."""
74
+ error = InvalidPDFFormatError(
75
+ message="Not a PDF",
76
+ document_id="doc-123"
77
+ )
78
+
79
+ self.assertEqual(error.error_type, "InvalidPDFFormat")
80
+ self.assertFalse(error.recoverable)
81
+
82
+ def test_with_details(self):
83
+ """Test error with additional details."""
84
+ error = InvalidPDFFormatError(
85
+ message="Not a PDF",
86
+ document_id="doc-123",
87
+ details={'detected_type': 'HTML'}
88
+ )
89
+
90
+ self.assertEqual(error.details['detected_type'], 'HTML')
91
+
92
+
93
+ class TestCorruptedPDFError(unittest.TestCase):
94
+ """Test cases for CorruptedPDFError."""
95
+
96
+ def test_error_type(self):
97
+ """Test that error type is set correctly."""
98
+ error = CorruptedPDFError(
99
+ message="PDF is corrupted",
100
+ document_id="doc-123"
101
+ )
102
+
103
+ self.assertEqual(error.error_type, "CorruptedPDF")
104
+ self.assertFalse(error.recoverable)
105
+
106
+
107
+ class TestEncryptedPDFError(unittest.TestCase):
108
+ """Test cases for EncryptedPDFError."""
109
+
110
+ def test_error_type(self):
111
+ """Test that error type is set correctly."""
112
+ error = EncryptedPDFError(
113
+ message="PDF is encrypted",
114
+ document_id="doc-123"
115
+ )
116
+
117
+ self.assertEqual(error.error_type, "EncryptedPDF")
118
+ self.assertFalse(error.recoverable)
119
+
120
+
121
+ class TestS3AccessDeniedError(unittest.TestCase):
122
+ """Test cases for S3AccessDeniedError."""
123
+
124
+ def test_error_type(self):
125
+ """Test that error type is set correctly."""
126
+ error = S3AccessDeniedError(
127
+ message="Access denied",
128
+ document_id="doc-123",
129
+ bucket="test-bucket",
130
+ key="test-key"
131
+ )
132
+
133
+ self.assertEqual(error.error_type, "S3AccessDenied")
134
+ self.assertFalse(error.recoverable)
135
+ self.assertEqual(error.details['bucket'], "test-bucket")
136
+ self.assertEqual(error.details['key'], "test-key")
137
+
138
+
139
+ class TestS3NotFoundError(unittest.TestCase):
140
+ """Test cases for S3NotFoundError."""
141
+
142
+ def test_error_type(self):
143
+ """Test that error type is set correctly."""
144
+ error = S3NotFoundError(
145
+ message="Object not found",
146
+ document_id="doc-123",
147
+ bucket="test-bucket",
148
+ key="test-key"
149
+ )
150
+
151
+ self.assertEqual(error.error_type, "S3NotFound")
152
+ self.assertFalse(error.recoverable)
153
+
154
+
155
+ class TestS3ThrottlingError(unittest.TestCase):
156
+ """Test cases for S3ThrottlingError."""
157
+
158
+ def test_error_type(self):
159
+ """Test that error type is set correctly."""
160
+ error = S3ThrottlingError(
161
+ message="Request throttled",
162
+ document_id="doc-123"
163
+ )
164
+
165
+ self.assertEqual(error.error_type, "S3Throttling")
166
+ self.assertTrue(error.recoverable)
167
+
168
+
169
+ class TestDynamoDBWriteError(unittest.TestCase):
170
+ """Test cases for DynamoDBWriteError."""
171
+
172
+ def test_error_type(self):
173
+ """Test that error type is set correctly."""
174
+ error = DynamoDBWriteError(
175
+ message="Write failed",
176
+ document_id="doc-123"
177
+ )
178
+
179
+ self.assertEqual(error.error_type, "DynamoDBWriteError")
180
+ self.assertTrue(error.recoverable)
181
+
182
+
183
+ class TestChunkingTimeoutError(unittest.TestCase):
184
+ """Test cases for ChunkingTimeoutError."""
185
+
186
+ def test_error_type(self):
187
+ """Test that error type is set correctly."""
188
+ error = ChunkingTimeoutError(
189
+ message="Operation timed out",
190
+ document_id="doc-123"
191
+ )
192
+
193
+ self.assertEqual(error.error_type, "ChunkingTimeout")
194
+ self.assertFalse(error.recoverable)
195
+
196
+
197
+ class TestClassifyS3Error(unittest.TestCase):
198
+ """Test cases for classify_s3_error function."""
199
+
200
+ def test_access_denied(self):
201
+ """Test classification of AccessDenied error."""
202
+ error_response = {
203
+ 'Error': {'Code': 'AccessDenied', 'Message': 'Access Denied'}
204
+ }
205
+ client_error = ClientError(error_response, 'GetObject')
206
+
207
+ result = classify_s3_error(
208
+ client_error,
209
+ document_id="doc-123",
210
+ bucket="test-bucket",
211
+ key="test-key"
212
+ )
213
+
214
+ self.assertIsInstance(result, S3AccessDeniedError)
215
+ self.assertEqual(result.error_type, "S3AccessDenied")
216
+
217
+ def test_no_such_key(self):
218
+ """Test classification of NoSuchKey error."""
219
+ error_response = {
220
+ 'Error': {'Code': 'NoSuchKey', 'Message': 'Key not found'}
221
+ }
222
+ client_error = ClientError(error_response, 'GetObject')
223
+
224
+ result = classify_s3_error(
225
+ client_error,
226
+ document_id="doc-123",
227
+ bucket="test-bucket",
228
+ key="test-key"
229
+ )
230
+
231
+ self.assertIsInstance(result, S3NotFoundError)
232
+ self.assertEqual(result.error_type, "S3NotFound")
233
+
234
+ def test_throttling(self):
235
+ """Test classification of throttling error."""
236
+ error_response = {
237
+ 'Error': {'Code': 'SlowDown', 'Message': 'Slow down'}
238
+ }
239
+ client_error = ClientError(error_response, 'PutObject')
240
+
241
+ result = classify_s3_error(
242
+ client_error,
243
+ document_id="doc-123",
244
+ bucket="test-bucket",
245
+ key="test-key"
246
+ )
247
+
248
+ self.assertIsInstance(result, S3ThrottlingError)
249
+ self.assertEqual(result.error_type, "S3Throttling")
250
+ self.assertTrue(result.recoverable)
251
+
252
+ def test_generic_error(self):
253
+ """Test classification of generic S3 error."""
254
+ error_response = {
255
+ 'Error': {'Code': 'InternalError', 'Message': 'Internal error'}
256
+ }
257
+ client_error = ClientError(error_response, 'GetObject')
258
+
259
+ result = classify_s3_error(
260
+ client_error,
261
+ document_id="doc-123",
262
+ bucket="test-bucket",
263
+ key="test-key"
264
+ )
265
+
266
+ self.assertIsInstance(result, PDFChunkingError)
267
+ self.assertEqual(result.error_type, "S3Error_InternalError")
268
+
269
+
270
+ class TestClassifyPDFError(unittest.TestCase):
271
+ """Test cases for classify_pdf_error function."""
272
+
273
+ def test_encrypted_pdf(self):
274
+ """Test classification of encrypted PDF error."""
275
+ error = Exception("PDF is encrypted and requires a password")
276
+
277
+ result = classify_pdf_error(error, document_id="doc-123")
278
+
279
+ self.assertIsInstance(result, EncryptedPDFError)
280
+ self.assertEqual(result.error_type, "EncryptedPDF")
281
+
282
+ def test_invalid_pdf(self):
283
+ """Test classification of invalid PDF error."""
284
+ error = Exception("File is not a valid PDF")
285
+
286
+ result = classify_pdf_error(error, document_id="doc-123")
287
+
288
+ self.assertIsInstance(result, InvalidPDFFormatError)
289
+ self.assertEqual(result.error_type, "InvalidPDFFormat")
290
+
291
+ def test_corrupted_pdf(self):
292
+ """Test classification of corrupted PDF error."""
293
+ error = Exception("PDF file is corrupted and cannot be read")
294
+
295
+ result = classify_pdf_error(error, document_id="doc-123")
296
+
297
+ self.assertIsInstance(result, CorruptedPDFError)
298
+ self.assertEqual(result.error_type, "CorruptedPDF")
299
+
300
+ def test_generic_pdf_error(self):
301
+ """Test classification of generic PDF error."""
302
+ error = Exception("Unknown PDF error")
303
+
304
+ result = classify_pdf_error(error, document_id="doc-123")
305
+
306
+ self.assertIsInstance(result, PDFChunkingError)
307
+ self.assertEqual(result.error_type, "PDFProcessingError")
308
+
309
+
310
+ class TestCreateErrorResponse(unittest.TestCase):
311
+ """Test cases for create_error_response function."""
312
+
313
+ def test_error_response_structure(self):
314
+ """Test that error response has correct structure."""
315
+ error = InvalidPDFFormatError(
316
+ message="Not a PDF",
317
+ document_id="doc-123",
318
+ details={'detected_type': 'HTML'}
319
+ )
320
+
321
+ response = create_error_response("doc-123", error)
322
+
323
+ self.assertEqual(response['documentId'], "doc-123")
324
+ self.assertFalse(response['requiresChunking'])
325
+ self.assertIn('error', response)
326
+ self.assertEqual(response['error']['type'], "InvalidPDFFormat")
327
+ self.assertEqual(response['error']['message'], "Not a PDF")
328
+ self.assertFalse(response['error']['recoverable'])
329
+ self.assertEqual(response['error']['details']['detected_type'], 'HTML')
330
+
331
+
332
+ class TestValidatePDFMagicBytes(unittest.TestCase):
333
+ """Test cases for validate_pdf_magic_bytes function."""
334
+
335
+ def test_valid_pdf(self):
336
+ """Test validation with valid PDF magic bytes."""
337
+ valid_pdf = b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n'
338
+
339
+ # Should not raise
340
+ validate_pdf_magic_bytes(valid_pdf, "doc-123")
341
+
342
+ def test_invalid_pdf_html(self):
343
+ """Test validation with HTML file."""
344
+ html_file = b'<html><body>Not a PDF</body></html>'
345
+
346
+ with self.assertRaises(InvalidPDFFormatError) as context:
347
+ validate_pdf_magic_bytes(html_file, "doc-123")
348
+
349
+ self.assertIn('HTML', context.exception.details['detected_type'])
350
+
351
+ def test_invalid_pdf_zip(self):
352
+ """Test validation with ZIP file."""
353
+ zip_file = b'PK\x03\x04...'
354
+
355
+ with self.assertRaises(InvalidPDFFormatError) as context:
356
+ validate_pdf_magic_bytes(zip_file, "doc-123")
357
+
358
+ self.assertIn('ZIP', context.exception.details['detected_type'])
359
+
360
+ def test_empty_file(self):
361
+ """Test validation with empty file."""
362
+ with self.assertRaises(InvalidPDFFormatError) as context:
363
+ validate_pdf_magic_bytes(b'', "doc-123")
364
+
365
+ self.assertIn('empty', context.exception.message.lower())
366
+
367
+ def test_too_short_file(self):
368
+ """Test validation with file too short to be PDF."""
369
+ with self.assertRaises(InvalidPDFFormatError) as context:
370
+ validate_pdf_magic_bytes(b'%PDF', "doc-123")
371
+
372
+ self.assertIn('too small', context.exception.message.lower())
373
+
374
+
375
+ class TestRetryWithExponentialBackoff(unittest.TestCase):
376
+ """Test cases for retry_with_exponential_backoff decorator."""
377
+
378
+ def test_successful_call(self):
379
+ """Test that successful calls return immediately."""
380
+ call_count = 0
381
+
382
+ @retry_with_exponential_backoff(max_retries=3)
383
+ def successful_function():
384
+ nonlocal call_count
385
+ call_count += 1
386
+ return "success"
387
+
388
+ result = successful_function()
389
+
390
+ self.assertEqual(result, "success")
391
+ self.assertEqual(call_count, 1)
392
+
393
+ def test_retry_on_recoverable_error(self):
394
+ """Test that recoverable errors trigger retries."""
395
+ call_count = 0
396
+
397
+ @retry_with_exponential_backoff(
398
+ max_retries=2,
399
+ base_delay=0.01,
400
+ retryable_exceptions=(S3ThrottlingError,)
401
+ )
402
+ def failing_then_succeeding():
403
+ nonlocal call_count
404
+ call_count += 1
405
+ if call_count < 3:
406
+ raise S3ThrottlingError("Throttled", document_id="doc-123")
407
+ return "success"
408
+
409
+ result = failing_then_succeeding()
410
+
411
+ self.assertEqual(result, "success")
412
+ self.assertEqual(call_count, 3)
413
+
414
+ def test_max_retries_exceeded(self):
415
+ """Test that max retries raises exception."""
416
+ call_count = 0
417
+
418
+ @retry_with_exponential_backoff(
419
+ max_retries=2,
420
+ base_delay=0.01,
421
+ retryable_exceptions=(S3ThrottlingError,)
422
+ )
423
+ def always_failing():
424
+ nonlocal call_count
425
+ call_count += 1
426
+ raise S3ThrottlingError("Throttled", document_id="doc-123")
427
+
428
+ with self.assertRaises(S3ThrottlingError):
429
+ always_failing()
430
+
431
+ self.assertEqual(call_count, 3) # Initial + 2 retries
432
+
433
+ def test_non_retryable_error(self):
434
+ """Test that non-retryable errors are raised immediately."""
435
+ call_count = 0
436
+
437
+ @retry_with_exponential_backoff(
438
+ max_retries=3,
439
+ base_delay=0.01,
440
+ retryable_exceptions=(S3ThrottlingError,)
441
+ )
442
+ def non_retryable_error():
443
+ nonlocal call_count
444
+ call_count += 1
445
+ raise InvalidPDFFormatError("Not a PDF", document_id="doc-123")
446
+
447
+ with self.assertRaises(InvalidPDFFormatError):
448
+ non_retryable_error()
449
+
450
+ self.assertEqual(call_count, 1) # No retries
451
+
452
+
453
+ class TestLogError(unittest.TestCase):
454
+ """Test cases for log_error function."""
455
+
456
+ @patch('error_handling.logger')
457
+ def test_log_error_with_stack_trace(self, mock_logger):
458
+ """Test logging error with stack trace."""
459
+ error = InvalidPDFFormatError(
460
+ message="Not a PDF",
461
+ document_id="doc-123"
462
+ )
463
+
464
+ log_error(error, include_stack_trace=True)
465
+
466
+ mock_logger.error.assert_called_once()
467
+ call_args = mock_logger.error.call_args
468
+ self.assertIn("doc-123", call_args[0][0])
469
+ self.assertTrue(call_args[1]['exc_info'])
470
+
471
+ @patch('error_handling.logger')
472
+ def test_log_error_without_stack_trace(self, mock_logger):
473
+ """Test logging error without stack trace."""
474
+ error = InvalidPDFFormatError(
475
+ message="Not a PDF",
476
+ document_id="doc-123"
477
+ )
478
+
479
+ log_error(error, include_stack_trace=False)
480
+
481
+ mock_logger.error.assert_called_once()
482
+ call_args = mock_logger.error.call_args
483
+ self.assertNotIn('exc_info', call_args[1])
484
+
485
+
486
+ if __name__ == '__main__':
487
+ unittest.main()