pygeai-orchestration 0.1.0b2__py3-none-any.whl

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 (61) hide show
  1. pygeai_orchestration/__init__.py +99 -0
  2. pygeai_orchestration/cli/__init__.py +7 -0
  3. pygeai_orchestration/cli/__main__.py +11 -0
  4. pygeai_orchestration/cli/commands/__init__.py +13 -0
  5. pygeai_orchestration/cli/commands/base.py +192 -0
  6. pygeai_orchestration/cli/error_handler.py +123 -0
  7. pygeai_orchestration/cli/formatters.py +419 -0
  8. pygeai_orchestration/cli/geai_orch.py +270 -0
  9. pygeai_orchestration/cli/interactive.py +265 -0
  10. pygeai_orchestration/cli/texts/help.py +169 -0
  11. pygeai_orchestration/core/__init__.py +130 -0
  12. pygeai_orchestration/core/base/__init__.py +23 -0
  13. pygeai_orchestration/core/base/agent.py +121 -0
  14. pygeai_orchestration/core/base/geai_agent.py +144 -0
  15. pygeai_orchestration/core/base/geai_orchestrator.py +77 -0
  16. pygeai_orchestration/core/base/orchestrator.py +142 -0
  17. pygeai_orchestration/core/base/pattern.py +161 -0
  18. pygeai_orchestration/core/base/tool.py +149 -0
  19. pygeai_orchestration/core/common/__init__.py +18 -0
  20. pygeai_orchestration/core/common/context.py +140 -0
  21. pygeai_orchestration/core/common/memory.py +176 -0
  22. pygeai_orchestration/core/common/message.py +50 -0
  23. pygeai_orchestration/core/common/state.py +181 -0
  24. pygeai_orchestration/core/composition.py +190 -0
  25. pygeai_orchestration/core/config.py +356 -0
  26. pygeai_orchestration/core/exceptions.py +400 -0
  27. pygeai_orchestration/core/handlers.py +380 -0
  28. pygeai_orchestration/core/utils/__init__.py +37 -0
  29. pygeai_orchestration/core/utils/cache.py +138 -0
  30. pygeai_orchestration/core/utils/config.py +94 -0
  31. pygeai_orchestration/core/utils/logging.py +57 -0
  32. pygeai_orchestration/core/utils/metrics.py +184 -0
  33. pygeai_orchestration/core/utils/validators.py +140 -0
  34. pygeai_orchestration/dev/__init__.py +15 -0
  35. pygeai_orchestration/dev/debug.py +288 -0
  36. pygeai_orchestration/dev/templates.py +321 -0
  37. pygeai_orchestration/dev/testing.py +301 -0
  38. pygeai_orchestration/patterns/__init__.py +15 -0
  39. pygeai_orchestration/patterns/multi_agent.py +237 -0
  40. pygeai_orchestration/patterns/planning.py +219 -0
  41. pygeai_orchestration/patterns/react.py +221 -0
  42. pygeai_orchestration/patterns/reflection.py +134 -0
  43. pygeai_orchestration/patterns/tool_use.py +170 -0
  44. pygeai_orchestration/tests/__init__.py +1 -0
  45. pygeai_orchestration/tests/test_base_classes.py +187 -0
  46. pygeai_orchestration/tests/test_cache.py +184 -0
  47. pygeai_orchestration/tests/test_cli_formatters.py +232 -0
  48. pygeai_orchestration/tests/test_common.py +214 -0
  49. pygeai_orchestration/tests/test_composition.py +265 -0
  50. pygeai_orchestration/tests/test_config.py +301 -0
  51. pygeai_orchestration/tests/test_dev_utils.py +337 -0
  52. pygeai_orchestration/tests/test_exceptions.py +327 -0
  53. pygeai_orchestration/tests/test_handlers.py +307 -0
  54. pygeai_orchestration/tests/test_metrics.py +171 -0
  55. pygeai_orchestration/tests/test_patterns.py +165 -0
  56. pygeai_orchestration-0.1.0b2.dist-info/METADATA +290 -0
  57. pygeai_orchestration-0.1.0b2.dist-info/RECORD +61 -0
  58. pygeai_orchestration-0.1.0b2.dist-info/WHEEL +5 -0
  59. pygeai_orchestration-0.1.0b2.dist-info/entry_points.txt +2 -0
  60. pygeai_orchestration-0.1.0b2.dist-info/licenses/LICENSE +8 -0
  61. pygeai_orchestration-0.1.0b2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,327 @@
1
+ """
2
+ Tests for custom exception classes.
3
+ """
4
+
5
+ import unittest
6
+ from pygeai_orchestration.core.exceptions import (
7
+ OrchestrationError,
8
+ PatternExecutionError,
9
+ PatternConfigurationError,
10
+ AgentError,
11
+ ToolExecutionError,
12
+ StateError,
13
+ ValidationError,
14
+ )
15
+
16
+
17
+ class TestExceptionHierarchy(unittest.TestCase):
18
+ """Test exception inheritance and base functionality."""
19
+
20
+ def test_orchestration_error_base(self):
21
+ """Test base OrchestrationError."""
22
+ error = OrchestrationError("Test error")
23
+ self.assertIsInstance(error, Exception)
24
+ self.assertEqual(str(error), "Test error")
25
+
26
+ def test_pattern_execution_error_inheritance(self):
27
+ """Test PatternExecutionError inherits from OrchestrationError."""
28
+ error = PatternExecutionError("Pattern failed")
29
+ self.assertIsInstance(error, OrchestrationError)
30
+ self.assertIsInstance(error, Exception)
31
+
32
+ def test_all_exceptions_inherit_from_base(self):
33
+ """Test all custom exceptions inherit from OrchestrationError."""
34
+ exceptions = [
35
+ PatternExecutionError("test"),
36
+ PatternConfigurationError("test"),
37
+ AgentError("test"),
38
+ ToolExecutionError("test"),
39
+ StateError("test"),
40
+ ValidationError("test"),
41
+ ]
42
+
43
+ for exc in exceptions:
44
+ self.assertIsInstance(exc, OrchestrationError)
45
+
46
+
47
+ class TestPatternExecutionError(unittest.TestCase):
48
+ """Test PatternExecutionError functionality."""
49
+
50
+ def test_basic_error(self):
51
+ """Test basic error creation."""
52
+ error = PatternExecutionError("Execution failed")
53
+ self.assertEqual(str(error), "Execution failed")
54
+
55
+ def test_error_with_pattern_name(self):
56
+ """Test error with pattern name."""
57
+ error = PatternExecutionError(
58
+ "Execution failed",
59
+ pattern_name="reflection"
60
+ )
61
+ self.assertIn("reflection", str(error))
62
+ self.assertEqual(error.pattern_name, "reflection")
63
+
64
+ def test_error_with_iteration(self):
65
+ """Test error with iteration number."""
66
+ error = PatternExecutionError(
67
+ "Execution failed",
68
+ pattern_name="reflection",
69
+ iteration=5
70
+ )
71
+ self.assertIn("5", str(error))
72
+ self.assertEqual(error.iteration, 5)
73
+
74
+ def test_error_with_details(self):
75
+ """Test error with additional details."""
76
+ details = {"max_iterations": 3, "timeout": True}
77
+ error = PatternExecutionError(
78
+ "Execution failed",
79
+ pattern_name="reflection",
80
+ iteration=5,
81
+ details=details
82
+ )
83
+ self.assertEqual(error.details, details)
84
+ self.assertIn("max_iterations", str(error))
85
+
86
+ def test_error_string_formatting(self):
87
+ """Test formatted error message."""
88
+ error = PatternExecutionError(
89
+ "Pattern execution failed",
90
+ pattern_name="reflection",
91
+ iteration=3,
92
+ details={"error": "timeout"}
93
+ )
94
+ error_str = str(error)
95
+ self.assertIn("Pattern execution failed", error_str)
96
+ self.assertIn("Pattern: reflection", error_str)
97
+ self.assertIn("Iteration: 3", error_str)
98
+ self.assertIn("Details:", error_str)
99
+
100
+
101
+ class TestPatternConfigurationError(unittest.TestCase):
102
+ """Test PatternConfigurationError functionality."""
103
+
104
+ def test_basic_error(self):
105
+ """Test basic configuration error."""
106
+ error = PatternConfigurationError("Invalid configuration")
107
+ self.assertEqual(str(error), "Invalid configuration")
108
+
109
+ def test_error_with_field(self):
110
+ """Test error with config field."""
111
+ error = PatternConfigurationError(
112
+ "Invalid value",
113
+ config_field="max_iterations"
114
+ )
115
+ self.assertIn("max_iterations", str(error))
116
+ self.assertEqual(error.config_field, "max_iterations")
117
+
118
+ def test_error_with_expected_received(self):
119
+ """Test error with expected and received values."""
120
+ error = PatternConfigurationError(
121
+ "Invalid max_iterations",
122
+ config_field="max_iterations",
123
+ expected="positive integer",
124
+ received=-1
125
+ )
126
+ self.assertIn("positive integer", str(error))
127
+ self.assertIn("-1", str(error))
128
+ self.assertEqual(error.expected, "positive integer")
129
+ self.assertEqual(error.received, -1)
130
+
131
+
132
+ class TestAgentError(unittest.TestCase):
133
+ """Test AgentError functionality."""
134
+
135
+ def test_basic_error(self):
136
+ """Test basic agent error."""
137
+ error = AgentError("Agent failed")
138
+ self.assertEqual(str(error), "Agent failed")
139
+
140
+ def test_error_with_agent_name(self):
141
+ """Test error with agent name."""
142
+ error = AgentError(
143
+ "Generation failed",
144
+ agent_name="research-agent"
145
+ )
146
+ self.assertIn("research-agent", str(error))
147
+ self.assertEqual(error.agent_name, "research-agent")
148
+
149
+ def test_error_with_operation(self):
150
+ """Test error with operation name."""
151
+ error = AgentError(
152
+ "Operation failed",
153
+ agent_name="research-agent",
154
+ operation="generate"
155
+ )
156
+ self.assertIn("generate", str(error))
157
+ self.assertEqual(error.operation, "generate")
158
+
159
+ def test_error_with_details(self):
160
+ """Test error with additional details."""
161
+ details = {"timeout": True, "retry_count": 3}
162
+ error = AgentError(
163
+ "Agent failed",
164
+ agent_name="research-agent",
165
+ operation="generate",
166
+ details=details
167
+ )
168
+ self.assertEqual(error.details, details)
169
+
170
+
171
+ class TestToolExecutionError(unittest.TestCase):
172
+ """Test ToolExecutionError functionality."""
173
+
174
+ def test_basic_error(self):
175
+ """Test basic tool error."""
176
+ error = ToolExecutionError("Tool failed")
177
+ self.assertEqual(str(error), "Tool failed")
178
+
179
+ def test_error_with_tool_name(self):
180
+ """Test error with tool name."""
181
+ error = ToolExecutionError(
182
+ "Execution failed",
183
+ tool_name="calculator"
184
+ )
185
+ self.assertIn("calculator", str(error))
186
+ self.assertEqual(error.tool_name, "calculator")
187
+
188
+ def test_error_with_input_data(self):
189
+ """Test error with input data."""
190
+ input_data = {"expression": "2 + + 2"}
191
+ error = ToolExecutionError(
192
+ "Invalid expression",
193
+ tool_name="calculator",
194
+ input_data=input_data
195
+ )
196
+ self.assertEqual(error.input_data, input_data)
197
+ self.assertIn("expression", str(error))
198
+
199
+ def test_error_with_details(self):
200
+ """Test error with additional details."""
201
+ details = {"error_type": "SyntaxError"}
202
+ error = ToolExecutionError(
203
+ "Execution failed",
204
+ tool_name="calculator",
205
+ details=details
206
+ )
207
+ self.assertEqual(error.details, details)
208
+
209
+
210
+ class TestStateError(unittest.TestCase):
211
+ """Test StateError functionality."""
212
+
213
+ def test_basic_error(self):
214
+ """Test basic state error."""
215
+ error = StateError("State error")
216
+ self.assertEqual(str(error), "State error")
217
+
218
+ def test_error_with_states(self):
219
+ """Test error with state information."""
220
+ error = StateError(
221
+ "Invalid transition",
222
+ current_state="running",
223
+ attempted_state="completed"
224
+ )
225
+ self.assertIn("running", str(error))
226
+ self.assertIn("completed", str(error))
227
+ self.assertEqual(error.current_state, "running")
228
+ self.assertEqual(error.attempted_state, "completed")
229
+
230
+ def test_error_with_details(self):
231
+ """Test error with additional details."""
232
+ details = {"reason": "tasks pending"}
233
+ error = StateError(
234
+ "Cannot transition",
235
+ current_state="running",
236
+ attempted_state="completed",
237
+ details=details
238
+ )
239
+ self.assertEqual(error.details, details)
240
+
241
+
242
+ class TestValidationError(unittest.TestCase):
243
+ """Test ValidationError functionality."""
244
+
245
+ def test_basic_error(self):
246
+ """Test basic validation error."""
247
+ error = ValidationError("Validation failed")
248
+ self.assertEqual(str(error), "Validation failed")
249
+
250
+ def test_error_with_field(self):
251
+ """Test error with field information."""
252
+ error = ValidationError(
253
+ "Invalid value",
254
+ field="task"
255
+ )
256
+ self.assertIn("task", str(error))
257
+ self.assertEqual(error.field, "task")
258
+
259
+ def test_error_with_expected_received(self):
260
+ """Test error with expected and received values."""
261
+ error = ValidationError(
262
+ "Invalid task",
263
+ field="task",
264
+ expected="non-empty string",
265
+ received=""
266
+ )
267
+ self.assertIn("non-empty string", str(error))
268
+ self.assertEqual(error.expected, "non-empty string")
269
+ self.assertEqual(error.received, "")
270
+
271
+ def test_error_with_example(self):
272
+ """Test error with example."""
273
+ error = ValidationError(
274
+ "Invalid input",
275
+ field="task",
276
+ expected="non-empty string",
277
+ received="",
278
+ example='task="Summarize this"'
279
+ )
280
+ self.assertIn("Summarize", str(error))
281
+ self.assertEqual(error.example, 'task="Summarize this"')
282
+
283
+ def test_error_string_formatting(self):
284
+ """Test complete formatted error message."""
285
+ error = ValidationError(
286
+ "Invalid task input",
287
+ field="task",
288
+ expected="non-empty string",
289
+ received="",
290
+ example='task="Summarize document"'
291
+ )
292
+ error_str = str(error)
293
+ self.assertIn("Invalid task input", error_str)
294
+ self.assertIn("Field: task", error_str)
295
+ self.assertIn("Expected: non-empty string", error_str)
296
+ self.assertIn("Received:", error_str)
297
+ self.assertIn("Example:", error_str)
298
+
299
+
300
+ class TestExceptionCatching(unittest.TestCase):
301
+ """Test exception catching patterns."""
302
+
303
+ def test_catch_specific_exception(self):
304
+ """Test catching specific exception type."""
305
+ with self.assertRaises(PatternExecutionError):
306
+ raise PatternExecutionError("Test")
307
+
308
+ def test_catch_as_base_exception(self):
309
+ """Test catching any orchestration error."""
310
+ with self.assertRaises(OrchestrationError):
311
+ raise PatternExecutionError("Test")
312
+
313
+ def test_catch_all_orchestration_errors(self):
314
+ """Test catching different errors as base type."""
315
+ errors = [
316
+ PatternExecutionError("test"),
317
+ AgentError("test"),
318
+ ValidationError("test"),
319
+ ]
320
+
321
+ for error in errors:
322
+ with self.assertRaises(OrchestrationError):
323
+ raise error
324
+
325
+
326
+ if __name__ == "__main__":
327
+ unittest.main()
@@ -0,0 +1,307 @@
1
+ """
2
+ Tests for error and retry handlers.
3
+ """
4
+
5
+ import unittest
6
+ import logging
7
+ from pygeai_orchestration.core.handlers import ErrorHandler, RetryHandler
8
+ from pygeai_orchestration.core.exceptions import (
9
+ OrchestrationError,
10
+ PatternExecutionError,
11
+ )
12
+
13
+
14
+ class TestErrorHandlerDetection(unittest.TestCase):
15
+ """Test error detection functionality."""
16
+
17
+ def test_has_errors_with_error_key(self):
18
+ """Test detection of error key in dict."""
19
+ response = {"error": "Something went wrong"}
20
+ self.assertTrue(ErrorHandler.has_errors(response))
21
+
22
+ def test_has_errors_with_errors_key(self):
23
+ """Test detection of errors key in dict."""
24
+ response = {"errors": ["Error 1", "Error 2"]}
25
+ self.assertTrue(ErrorHandler.has_errors(response))
26
+
27
+ def test_has_errors_with_success_false(self):
28
+ """Test detection of success=False."""
29
+ response = {"success": False, "message": "Failed"}
30
+ self.assertTrue(ErrorHandler.has_errors(response))
31
+
32
+ def test_has_errors_with_error_status(self):
33
+ """Test detection of error status."""
34
+ self.assertTrue(ErrorHandler.has_errors({"status": "error"}))
35
+ self.assertTrue(ErrorHandler.has_errors({"status": "failed"}))
36
+ self.assertTrue(ErrorHandler.has_errors({"status": 500}))
37
+
38
+ def test_has_errors_with_error_code(self):
39
+ """Test detection of error codes."""
40
+ self.assertTrue(ErrorHandler.has_errors({"code": 400}))
41
+ self.assertTrue(ErrorHandler.has_errors({"code": 500}))
42
+ self.assertTrue(ErrorHandler.has_errors({"code": 404}))
43
+
44
+ def test_has_errors_with_exception(self):
45
+ """Test detection of Exception objects."""
46
+ error = Exception("Test error")
47
+ self.assertTrue(ErrorHandler.has_errors(error))
48
+
49
+ def test_has_no_errors_with_success_true(self):
50
+ """Test no error detection with success response."""
51
+ response = {"success": True, "data": "result"}
52
+ self.assertFalse(ErrorHandler.has_errors(response))
53
+
54
+ def test_has_no_errors_with_good_status(self):
55
+ """Test no error detection with 2xx status."""
56
+ response = {"status": 200}
57
+ self.assertFalse(ErrorHandler.has_errors(response))
58
+
59
+ def test_has_no_errors_with_none(self):
60
+ """Test no error detection with None."""
61
+ self.assertFalse(ErrorHandler.has_errors(None))
62
+
63
+ def test_has_no_errors_with_empty_dict(self):
64
+ """Test no error detection with empty dict."""
65
+ self.assertFalse(ErrorHandler.has_errors({}))
66
+
67
+
68
+ class TestErrorHandlerExtraction(unittest.TestCase):
69
+ """Test error extraction functionality."""
70
+
71
+ def test_extract_error_from_dict_with_error_key(self):
72
+ """Test extracting error from dict with error key."""
73
+ response = {"error": "Connection failed"}
74
+ error = ErrorHandler.extract_error(response)
75
+ self.assertEqual(error, "Connection failed")
76
+
77
+ def test_extract_error_from_dict_with_nested_error(self):
78
+ """Test extracting from nested error object."""
79
+ response = {"error": {"message": "Invalid input", "code": 400}}
80
+ error = ErrorHandler.extract_error(response)
81
+ self.assertIn("Invalid input", error)
82
+
83
+ def test_extract_error_from_errors_list(self):
84
+ """Test extracting from errors list."""
85
+ response = {"errors": ["Error 1", "Error 2"]}
86
+ error = ErrorHandler.extract_error(response)
87
+ self.assertIn("Error 1", error)
88
+
89
+ def test_extract_error_from_message_key(self):
90
+ """Test extracting from message key."""
91
+ response = {"message": "Operation failed"}
92
+ error = ErrorHandler.extract_error(response)
93
+ self.assertEqual(error, "Operation failed")
94
+
95
+ def test_extract_error_from_status_code(self):
96
+ """Test extracting from status/code."""
97
+ response = {"status": 500, "code": "SERVER_ERROR"}
98
+ error = ErrorHandler.extract_error(response)
99
+ self.assertIn("500", error)
100
+ self.assertIn("SERVER_ERROR", error)
101
+
102
+ def test_extract_error_from_exception(self):
103
+ """Test extracting from Exception."""
104
+ exc = ValueError("Invalid value")
105
+ error = ErrorHandler.extract_error(exc)
106
+ self.assertEqual(error, "Invalid value")
107
+
108
+ def test_extract_error_from_string(self):
109
+ """Test extracting from string."""
110
+ error = ErrorHandler.extract_error("Simple error message")
111
+ self.assertEqual(error, "Simple error message")
112
+
113
+ def test_extract_error_from_none(self):
114
+ """Test extracting from None."""
115
+ error = ErrorHandler.extract_error(None)
116
+ self.assertIn("None", error)
117
+
118
+
119
+ class TestErrorHandlerHandling(unittest.TestCase):
120
+ """Test error handling functionality."""
121
+
122
+ def test_handle_error_basic(self):
123
+ """Test basic error handling."""
124
+ error = {"error": "Test error"}
125
+ message = ErrorHandler.handle_error(error)
126
+ self.assertEqual(message, "Test error")
127
+
128
+ def test_handle_error_with_context(self):
129
+ """Test error handling with context."""
130
+ error = {"error": "Operation failed"}
131
+ message = ErrorHandler.handle_error(
132
+ error,
133
+ context={"operation": "generate", "attempt": 1}
134
+ )
135
+ self.assertIn("Operation failed", message)
136
+ self.assertIn("operation=generate", message)
137
+ self.assertIn("attempt=1", message)
138
+
139
+ def test_handle_error_with_log_level(self):
140
+ """Test error handling with different log levels."""
141
+ error = {"error": "Warning message"}
142
+ # Should not raise, just return message
143
+ message = ErrorHandler.handle_error(error, log_level=logging.WARNING)
144
+ self.assertEqual(message, "Warning message")
145
+
146
+
147
+ class TestErrorHandlerWrap(unittest.TestCase):
148
+ """Test exception wrapping functionality."""
149
+
150
+ def test_wrap_exception_basic(self):
151
+ """Test basic exception wrapping."""
152
+ original = ValueError("Original error")
153
+ wrapped = ErrorHandler.wrap_exception(original)
154
+
155
+ self.assertIsInstance(wrapped, OrchestrationError)
156
+ self.assertIn("Original error", str(wrapped))
157
+ self.assertEqual(wrapped.__cause__, original)
158
+
159
+ def test_wrap_exception_with_custom_class(self):
160
+ """Test wrapping with custom exception class."""
161
+ original = ValueError("Original error")
162
+ wrapped = ErrorHandler.wrap_exception(
163
+ original,
164
+ exception_class=PatternExecutionError,
165
+ pattern_name="reflection"
166
+ )
167
+
168
+ self.assertIsInstance(wrapped, PatternExecutionError)
169
+ self.assertEqual(wrapped.pattern_name, "reflection")
170
+
171
+ def test_wrap_exception_with_prefix(self):
172
+ """Test wrapping with message prefix."""
173
+ original = ValueError("Original error")
174
+ wrapped = ErrorHandler.wrap_exception(
175
+ original,
176
+ message_prefix="Pattern failed"
177
+ )
178
+
179
+ self.assertIn("Pattern failed", str(wrapped))
180
+ self.assertIn("Original error", str(wrapped))
181
+
182
+
183
+ class TestErrorHandlerRecoverable(unittest.TestCase):
184
+ """Test recoverable error detection."""
185
+
186
+ def test_is_recoverable_timeout(self):
187
+ """Test timeout errors are recoverable."""
188
+ self.assertTrue(ErrorHandler.is_recoverable({"code": 504}))
189
+ self.assertTrue(ErrorHandler.is_recoverable({"error": "Request timeout"}))
190
+
191
+ def test_is_recoverable_rate_limit(self):
192
+ """Test rate limit errors are recoverable."""
193
+ self.assertTrue(ErrorHandler.is_recoverable({"code": 429}))
194
+ self.assertTrue(ErrorHandler.is_recoverable({"error": "Rate limit exceeded"}))
195
+
196
+ def test_is_recoverable_service_unavailable(self):
197
+ """Test service unavailable is recoverable."""
198
+ self.assertTrue(ErrorHandler.is_recoverable({"code": 503}))
199
+ self.assertTrue(ErrorHandler.is_recoverable({"error": "Service unavailable"}))
200
+
201
+ def test_is_not_recoverable_bad_request(self):
202
+ """Test bad request is not recoverable."""
203
+ self.assertFalse(ErrorHandler.is_recoverable({"code": 400}))
204
+
205
+ def test_is_not_recoverable_not_found(self):
206
+ """Test not found is not recoverable."""
207
+ self.assertFalse(ErrorHandler.is_recoverable({"code": 404}))
208
+
209
+ def test_is_recoverable_exception(self):
210
+ """Test exception recoverability."""
211
+ timeout_exc = TimeoutError("Connection timeout")
212
+ self.assertTrue(ErrorHandler.is_recoverable(timeout_exc))
213
+
214
+ value_exc = ValueError("Invalid value")
215
+ self.assertFalse(ErrorHandler.is_recoverable(value_exc))
216
+
217
+
218
+ class TestErrorHandlerFormat(unittest.TestCase):
219
+ """Test error response formatting."""
220
+
221
+ def test_format_error_response_basic(self):
222
+ """Test basic error formatting."""
223
+ error = ValueError("Test error")
224
+ response = ErrorHandler.format_error_response(error)
225
+
226
+ self.assertFalse(response['success'])
227
+ self.assertEqual(response['error'], "Test error")
228
+ self.assertEqual(response['error_type'], "ValueError")
229
+
230
+ def test_format_error_response_with_details(self):
231
+ """Test formatting with details."""
232
+ error = {"error": "Failed", "code": 500, "details": {"reason": "timeout"}}
233
+ response = ErrorHandler.format_error_response(error)
234
+
235
+ self.assertFalse(response['success'])
236
+ self.assertEqual(response['error'], "Failed")
237
+ self.assertEqual(response['code'], 500)
238
+ self.assertEqual(response['details'], {"reason": "timeout"})
239
+
240
+ def test_format_error_response_without_details(self):
241
+ """Test formatting without details."""
242
+ error = ValueError("Test error")
243
+ response = ErrorHandler.format_error_response(error, include_details=False)
244
+
245
+ self.assertFalse(response['success'])
246
+ self.assertEqual(response['error'], "Test error")
247
+ self.assertNotIn('error_type', response)
248
+
249
+ def test_format_error_response_with_cause(self):
250
+ """Test formatting with exception cause."""
251
+ original = ValueError("Original")
252
+ wrapped = ErrorHandler.wrap_exception(original, message_prefix="Wrapped")
253
+ response = ErrorHandler.format_error_response(wrapped)
254
+
255
+ self.assertEqual(response['original_error'], "Original")
256
+
257
+
258
+ class TestRetryHandler(unittest.TestCase):
259
+ """Test retry handler functionality."""
260
+
261
+ def test_retry_handler_init(self):
262
+ """Test retry handler initialization."""
263
+ handler = RetryHandler(max_retries=5, delay=2.0)
264
+ self.assertEqual(handler.max_retries, 5)
265
+ self.assertEqual(handler.delay, 2.0)
266
+
267
+ def test_should_retry_within_limit(self):
268
+ """Test should retry when within limit."""
269
+ handler = RetryHandler(max_retries=3, recoverable_only=False)
270
+ self.assertTrue(handler.should_retry({"error": "test"}, attempt=0))
271
+ self.assertTrue(handler.should_retry({"error": "test"}, attempt=2))
272
+
273
+ def test_should_not_retry_at_limit(self):
274
+ """Test should not retry when at limit."""
275
+ handler = RetryHandler(max_retries=3, recoverable_only=False)
276
+ self.assertFalse(handler.should_retry({"error": "test"}, attempt=3))
277
+
278
+ def test_should_retry_recoverable_only(self):
279
+ """Test retry only recoverable errors."""
280
+ handler = RetryHandler(max_retries=3, recoverable_only=True)
281
+
282
+ # Recoverable error
283
+ self.assertTrue(handler.should_retry({"code": 504}, attempt=0))
284
+
285
+ # Non-recoverable error
286
+ self.assertFalse(handler.should_retry({"code": 400}, attempt=0))
287
+
288
+ def test_get_delay_exponential_backoff(self):
289
+ """Test exponential backoff delay calculation."""
290
+ handler = RetryHandler(delay=1.0, backoff_factor=2.0)
291
+
292
+ self.assertEqual(handler.get_delay(0), 1.0)
293
+ self.assertEqual(handler.get_delay(1), 2.0)
294
+ self.assertEqual(handler.get_delay(2), 4.0)
295
+ self.assertEqual(handler.get_delay(3), 8.0)
296
+
297
+ def test_get_delay_custom_backoff(self):
298
+ """Test custom backoff factor."""
299
+ handler = RetryHandler(delay=1.0, backoff_factor=3.0)
300
+
301
+ self.assertEqual(handler.get_delay(0), 1.0)
302
+ self.assertEqual(handler.get_delay(1), 3.0)
303
+ self.assertEqual(handler.get_delay(2), 9.0)
304
+
305
+
306
+ if __name__ == "__main__":
307
+ unittest.main()