@@ -166,3 +166,173 @@ def test_given_both_from_exception_and_as_sanitized_airbyte_message_with_stream_
166166 )
167167 message = traced_exc .as_sanitized_airbyte_message (stream_descriptor = _ANOTHER_STREAM_DESCRIPTOR )
168168 assert message .trace .error .stream_descriptor == _A_STREAM_DESCRIPTOR
169+
170+
171+ class TestAirbyteTracedExceptionStr :
172+ """Tests proving that __str__ returns user-facing message instead of internal_message."""
173+
174+ def test_str_returns_user_facing_message_when_both_set (self ) -> None :
175+ exc = AirbyteTracedException (
176+ internal_message = "raw API error: 401 Unauthorized" ,
177+ message = "Authentication credentials are invalid." ,
178+ )
179+ assert str (exc ) == "Authentication credentials are invalid."
180+
181+ def test_str_falls_back_to_internal_message_when_message_is_none (self ) -> None :
182+ exc = AirbyteTracedException (internal_message = "an internal error" )
183+ assert str (exc ) == "an internal error"
184+
185+ def test_str_returns_empty_string_when_both_none (self ) -> None :
186+ exc = AirbyteTracedException ()
187+ assert str (exc ) == ""
188+
189+ def test_str_returns_message_when_internal_message_is_none (self ) -> None :
190+ exc = AirbyteTracedException (message = "A user-friendly error occurred." )
191+ assert str (exc ) == "A user-friendly error occurred."
192+
193+ def test_str_used_in_fstring_returns_user_facing_message (self ) -> None :
194+ exc = AirbyteTracedException (
195+ internal_message = "internal detail" ,
196+ message = "Connection timed out." ,
197+ )
198+ assert f"Error: { exc } " == "Error: Connection timed out."
199+
200+ def test_str_used_in_logging_format_returns_user_facing_message (self ) -> None :
201+ exc = AirbyteTracedException (
202+ internal_message = "socket.timeout: read timed out" ,
203+ message = "Request timed out." ,
204+ )
205+ assert "Error: %s" % exc == "Error: Request timed out."
206+
207+ def test_args_still_contains_internal_message (self ) -> None :
208+ """Verify args[0] is still internal_message for traceback formatting."""
209+ exc = AirbyteTracedException (
210+ internal_message = "internal detail" ,
211+ message = "user-facing message" ,
212+ )
213+ assert exc .args [0 ] == "internal detail"
214+
215+ def test_str_on_subclass_inherits_behavior (self ) -> None :
216+ """Verify subclasses inherit the __str__ override without needing their own."""
217+
218+ class CustomTracedException (AirbyteTracedException ):
219+ pass
220+
221+ exc = CustomTracedException (
222+ internal_message = "raw error" ,
223+ message = "User-friendly error." ,
224+ )
225+ assert str (exc ) == "User-friendly error."
226+
227+ def test_str_with_from_exception_factory (self ) -> None :
228+ original = ValueError ("original error" )
229+ exc = AirbyteTracedException .from_exception (
230+ original , message = "A validation error occurred."
231+ )
232+ assert str (exc ) == "A validation error occurred."
233+ assert exc .internal_message == "original error"
234+
235+ def test_str_with_from_exception_without_message (self ) -> None :
236+ original = RuntimeError ("runtime failure" )
237+ exc = AirbyteTracedException .from_exception (original )
238+ assert str (exc ) == "runtime failure"
239+
240+ def test_stack_trace_uses_str_representation (self ) -> None :
241+ """Verify traceback one-liner uses __str__ (user-facing message)."""
242+ exc = AirbyteTracedException (
243+ internal_message = "internal detail for traceback" ,
244+ message = "User sees this." ,
245+ )
246+ airbyte_message = exc .as_airbyte_message ()
247+ assert "User sees this." in airbyte_message .trace .error .stack_trace
248+
249+ def test_str_with_empty_message_does_not_fall_back_to_internal_message (self ) -> None :
250+ """Explicit empty message should be respected and not replaced by internal_message."""
251+ exc = AirbyteTracedException (
252+ internal_message = "an internal error that should not be shown to the user" ,
253+ message = "" ,
254+ )
255+ assert str (exc ) == ""
256+
257+ def test_internal_message_preserved_in_trace_error (self ) -> None :
258+ """Verify internal_message is still available in the trace error for debugging."""
259+ exc = AirbyteTracedException (
260+ internal_message = "raw API error: 401" ,
261+ message = "Authentication failed." ,
262+ )
263+ airbyte_message = exc .as_airbyte_message ()
264+ assert airbyte_message .trace .error .internal_message == "raw API error: 401"
265+ assert airbyte_message .trace .error .message == "Authentication failed."
266+
267+
268+ class TestFromExceptionPreservesFields :
269+ """Tests proving that from_exception preserves both fields when wrapping an AirbyteTracedException."""
270+
271+ def test_from_exception_wrapping_traced_preserves_internal_message (self ) -> None :
272+ """When wrapping an AirbyteTracedException, internal_message should be taken directly, not via str()."""
273+ original = AirbyteTracedException (
274+ internal_message = "raw API error: 401 Unauthorized" ,
275+ message = "Authentication failed." ,
276+ )
277+ wrapped = AirbyteTracedException .from_exception (original )
278+ assert wrapped .internal_message == "raw API error: 401 Unauthorized"
279+
280+ def test_from_exception_wrapping_traced_preserves_message (self ) -> None :
281+ """When wrapping an AirbyteTracedException without explicit message, the original's message is preserved."""
282+ original = AirbyteTracedException (
283+ internal_message = "raw API error" ,
284+ message = "User-friendly error." ,
285+ )
286+ wrapped = AirbyteTracedException .from_exception (original )
287+ assert wrapped .message == "User-friendly error."
288+
289+ def test_from_exception_wrapping_traced_caller_message_overrides (self ) -> None :
290+ """When the caller provides an explicit message, it should override the original's message."""
291+ original = AirbyteTracedException (
292+ internal_message = "raw API error" ,
293+ message = "Original user message." ,
294+ )
295+ wrapped = AirbyteTracedException .from_exception (original , message = "Custom wrapper message." )
296+ assert wrapped .message == "Custom wrapper message."
297+ assert wrapped .internal_message == "raw API error"
298+
299+ def test_from_exception_wrapping_traced_with_only_message (self ) -> None :
300+ """When wrapping a traced exception that has message but no internal_message, both fields are preserved."""
301+ original = AirbyteTracedException (
302+ message = "User error only." ,
303+ )
304+ wrapped = AirbyteTracedException .from_exception (original )
305+ assert wrapped .internal_message is None
306+ assert wrapped .message == "User error only."
307+
308+ def test_from_exception_wrapping_traced_with_only_internal_message (self ) -> None :
309+ """When wrapping a traced exception that has only internal_message, it is preserved correctly."""
310+ original = AirbyteTracedException (
311+ internal_message = "internal detail only" ,
312+ )
313+ wrapped = AirbyteTracedException .from_exception (original )
314+ assert wrapped .internal_message == "internal detail only"
315+ assert wrapped .message is None
316+
317+ def test_from_exception_wrapping_traced_with_neither_field (self ) -> None :
318+ """When wrapping a traced exception with no messages, both remain None."""
319+ original = AirbyteTracedException ()
320+ wrapped = AirbyteTracedException .from_exception (original )
321+ assert wrapped .internal_message is None
322+ assert wrapped .message is None
323+
324+ def test_from_exception_wrapping_regular_exception_unchanged (self ) -> None :
325+ """Wrapping a regular exception should still use str(exc) for internal_message."""
326+ original = ValueError ("some value error" )
327+ wrapped = AirbyteTracedException .from_exception (original )
328+ assert wrapped .internal_message == "some value error"
329+ assert wrapped .message is None
330+
331+ def test_from_exception_wrapping_regular_exception_with_message (self ) -> None :
332+ """Wrapping a regular exception with explicit message kwarg still works."""
333+ original = RuntimeError ("runtime failure" )
334+ wrapped = AirbyteTracedException .from_exception (
335+ original , message = "A runtime error occurred."
336+ )
337+ assert wrapped .internal_message == "runtime failure"
338+ assert wrapped .message == "A runtime error occurred."
0 commit comments