@@ -385,6 +385,112 @@ void constructFromProto_withInnerFailureAndProperties() {
385385 assertEquals (false , details .getInnerFailure ().getProperties ().get ("retryable" ));
386386 }
387387
388+ @ Test
389+ void fromException_withProvider_extractsProperties () {
390+ ExceptionPropertiesProvider provider = exception -> {
391+ if (exception instanceof IllegalArgumentException ) {
392+ Map <String , Object > props = new HashMap <>();
393+ props .put ("paramName" , exception .getMessage ());
394+ props .put ("severity" , 3 );
395+ props .put ("isCritical" , true );
396+ return props ;
397+ }
398+ return null ;
399+ };
400+
401+ IllegalArgumentException ex = new IllegalArgumentException ("userId" );
402+
403+ FailureDetails details = FailureDetails .fromException (ex , provider );
404+
405+ assertEquals ("java.lang.IllegalArgumentException" , details .getErrorType ());
406+ assertEquals ("userId" , details .getErrorMessage ());
407+ assertNotNull (details .getProperties ());
408+ assertEquals (3 , details .getProperties ().size ());
409+ assertEquals ("userId" , details .getProperties ().get ("paramName" ));
410+ assertEquals (3 , details .getProperties ().get ("severity" ));
411+ assertEquals (true , details .getProperties ().get ("isCritical" ));
412+ }
413+
414+ @ Test
415+ void fromException_withProvider_propertiesOnInnerCauseToo () {
416+ ExceptionPropertiesProvider provider = exception -> {
417+ Map <String , Object > props = new HashMap <>();
418+ props .put ("exceptionType" , exception .getClass ().getSimpleName ());
419+ return props ;
420+ };
421+
422+ IOException inner = new IOException ("disk full" );
423+ RuntimeException outer = new RuntimeException ("failed" , inner );
424+
425+ FailureDetails details = FailureDetails .fromException (outer , provider );
426+
427+ assertNotNull (details .getProperties ());
428+ assertEquals ("RuntimeException" , details .getProperties ().get ("exceptionType" ));
429+
430+ assertNotNull (details .getInnerFailure ());
431+ assertNotNull (details .getInnerFailure ().getProperties ());
432+ assertEquals ("IOException" , details .getInnerFailure ().getProperties ().get ("exceptionType" ));
433+ }
434+
435+ @ Test
436+ void fromException_withProvider_returnsNull_noProperties () {
437+ ExceptionPropertiesProvider provider = exception -> null ;
438+
439+ RuntimeException ex = new RuntimeException ("test" );
440+
441+ FailureDetails details = FailureDetails .fromException (ex , provider );
442+
443+ assertNull (details .getProperties ());
444+ }
445+
446+ @ Test
447+ void fromException_withNullProvider_noProperties () {
448+ RuntimeException ex = new RuntimeException ("test" );
449+
450+ FailureDetails details = FailureDetails .fromException (ex , null );
451+
452+ assertEquals ("java.lang.RuntimeException" , details .getErrorType ());
453+ assertEquals ("test" , details .getErrorMessage ());
454+ assertNull (details .getProperties ());
455+ }
456+
457+ @ Test
458+ void fromException_providerThrows_gracefullyIgnored () {
459+ ExceptionPropertiesProvider provider = exception -> {
460+ throw new RuntimeException ("provider error" );
461+ };
462+
463+ IllegalStateException ex = new IllegalStateException ("original error" );
464+
465+ FailureDetails details = FailureDetails .fromException (ex , provider );
466+
467+ assertEquals ("java.lang.IllegalStateException" , details .getErrorType ());
468+ assertEquals ("original error" , details .getErrorMessage ());
469+ assertNull (details .getProperties ());
470+ }
471+
472+ @ Test
473+ void fromException_withProvider_roundTripsViaProto () {
474+ ExceptionPropertiesProvider provider = exception -> {
475+ Map <String , Object > props = new HashMap <>();
476+ props .put ("errorCode" , "VALIDATION_FAILED" );
477+ props .put ("retryCount" , 3 );
478+ props .put ("isCritical" , true );
479+ return props ;
480+ };
481+
482+ IllegalArgumentException ex = new IllegalArgumentException ("bad input" );
483+ FailureDetails details = FailureDetails .fromException (ex , provider );
484+
485+ TaskFailureDetails proto = details .toProto ();
486+ FailureDetails roundTripped = new FailureDetails (proto );
487+
488+ assertNotNull (roundTripped .getProperties ());
489+ assertEquals ("VALIDATION_FAILED" , roundTripped .getProperties ().get ("errorCode" ));
490+ assertEquals (3.0 , roundTripped .getProperties ().get ("retryCount" ));
491+ assertEquals (true , roundTripped .getProperties ().get ("isCritical" ));
492+ }
493+
388494 @ Test
389495 void constructFromProto_withProperties_containsNullKey () {
390496 // Properties map with a null-valued entry should be preserved
0 commit comments