@@ -2814,7 +2814,29 @@ describe('parseConfigurationObject', () => {
28142814 roles : 1 ,
28152815 }
28162816
2817- const errorObject = [
2817+ const abortOrReport = vi . fn ( )
2818+ await parseConfigurationObject ( WebConfigurationSchema , 'tmp' , configurationObject , abortOrReport )
2819+
2820+ // Verify the function was called and capture the actual error structure
2821+ expect ( abortOrReport ) . toHaveBeenCalledOnce ( )
2822+ const callArgs = abortOrReport . mock . calls [ 0 ] !
2823+ const actualErrorMessage = callArgs [ 0 ]
2824+
2825+ // Convert TokenizedString to regular string for testing
2826+ const errorString = actualErrorMessage . value
2827+
2828+ // The enhanced union handling should show only the most relevant errors
2829+ // instead of showing all variants, making it much more user-friendly
2830+ expect ( errorString ) . toContain ( '[roles]: Expected array, received number' )
2831+
2832+ // Should NOT show the confusing union variant breakdown
2833+ expect ( errorString ) . not . toContain ( 'Union validation failed' )
2834+ expect ( errorString ) . not . toContain ( 'Variant 1:' )
2835+ expect ( errorString ) . not . toContain ( 'Variant 2:' )
2836+ } )
2837+
2838+ test ( 'parseHumanReadableError formats union errors with smart variant detection' , ( ) => {
2839+ const unionErrorObject = [
28182840 {
28192841 code : 'invalid_union' ,
28202842 unionErrors : [
@@ -2824,36 +2846,226 @@ describe('parseConfigurationObject', () => {
28242846 code : 'invalid_type' ,
28252847 expected : 'array' ,
28262848 received : 'number' ,
2827- path : [ 'roles' ] ,
2849+ path : [ 'web' , ' roles'] ,
28282850 message : 'Expected array, received number' ,
28292851 } ,
2852+ {
2853+ code : 'invalid_type' ,
2854+ expected : 'string' ,
2855+ received : 'undefined' ,
2856+ path : [ 'web' , 'commands' , 'build' ] ,
2857+ message : 'Required' ,
2858+ } ,
28302859 ] ,
28312860 name : 'ZodError' ,
28322861 } ,
28332862 {
28342863 issues : [
28352864 {
2836- expected : "'frontend' | 'backend' | 'background'" ,
2865+ code : 'invalid_literal' ,
2866+ expected : "'frontend'" ,
28372867 received : 'number' ,
2838- code : 'invalid_type' ,
2839- path : [ 'type' ] ,
2840- message : "Expected 'frontend' | 'backend' | 'background', received number" ,
2868+ path : [ 'web' , 'type' ] ,
2869+ message : "Invalid literal value, expected 'frontend'" ,
28412870 } ,
28422871 ] ,
28432872 name : 'ZodError' ,
28442873 } ,
28452874 ] ,
2875+ path : [ 'web' ] ,
2876+ message : 'Invalid input' ,
2877+ } ,
2878+ ]
2879+
2880+ const result = parseHumanReadableError ( unionErrorObject )
2881+
2882+ // Verify the enhanced format shows only the best matching variant's errors
2883+ // (Variant 1 has both missing field + type error, so it's likely the intended one)
2884+ expect ( result ) . toContain ( '[web.roles]: Expected array, received number' )
2885+ expect ( result ) . toContain ( '[web.commands.build]: Required' )
2886+
2887+ // Should NOT show confusing union variant breakdown
2888+ expect ( result ) . not . toContain ( 'Union validation failed' )
2889+ expect ( result ) . not . toContain ( 'Variant 1:' )
2890+ expect ( result ) . not . toContain ( 'Variant 2:' )
2891+
2892+ // Should NOT show errors from the less likely variant 2
2893+ expect ( result ) . not . toContain ( "[web.type]: Invalid literal value, expected 'frontend'" )
2894+ } )
2895+
2896+ test ( 'parseHumanReadableError handles regular non-union errors' , ( ) => {
2897+ const regularErrorObject = [
2898+ {
2899+ path : [ 'name' ] ,
2900+ message : 'Required field is missing' ,
2901+ } ,
2902+ {
2903+ path : [ 'version' ] ,
2904+ message : 'Must be a valid semver string' ,
2905+ } ,
2906+ ]
2907+
2908+ const result = parseHumanReadableError ( regularErrorObject )
2909+
2910+ // Verify regular errors still work as expected
2911+ expect ( result ) . toBe ( '• [name]: Required field is missing\n• [version]: Must be a valid semver string\n' )
2912+ expect ( result ) . not . toContain ( 'Union validation failed' )
2913+ } )
2914+
2915+ test ( 'parseHumanReadableError handles edge cases for union error detection' , ( ) => {
2916+ // Test case 1: Union error with no unionErrors array
2917+ const noUnionErrors = [
2918+ {
2919+ code : 'invalid_union' ,
2920+ path : [ 'root' ] ,
2921+ message : 'Invalid input' ,
2922+ } ,
2923+ ]
2924+
2925+ const result1 = parseHumanReadableError ( noUnionErrors )
2926+ expect ( result1 ) . toBe ( '• [root]: Invalid input\n' )
2927+
2928+ // Test case 2: Union error with empty unionErrors array - should fall back to showing full union error
2929+ const emptyUnionErrors = [
2930+ {
2931+ code : 'invalid_union' ,
2932+ unionErrors : [ ] ,
2933+ path : [ 'root' ] ,
2934+ message : 'Invalid input' ,
2935+ } ,
2936+ ]
2937+
2938+ const result2 = parseHumanReadableError ( emptyUnionErrors )
2939+ expect ( result2 ) . toContain ( 'Union validation failed. None of the possible variants matched:' )
2940+
2941+ // Test case 3: Union errors with variants that have no issues - results in empty string
2942+ const noIssuesVariants = [
2943+ {
2944+ code : 'invalid_union' ,
2945+ unionErrors : [
2946+ { issues : [ ] , name : 'ZodError' } ,
2947+ { issues : [ ] , name : 'ZodError' } ,
2948+ ] ,
2949+ path : [ 'root' ] ,
2950+ message : 'Invalid input' ,
2951+ } ,
2952+ ]
2953+
2954+ const result3 = parseHumanReadableError ( noIssuesVariants )
2955+ // When all variants have no issues, the best variant selection returns null issues
2956+ // resulting in no output, which falls back to the union error display
2957+ expect ( result3 ) . toContain ( 'Union validation failed. None of the possible variants matched:' )
2958+ } )
2959+
2960+ test ( 'findBestMatchingVariant scoring logic works correctly' , ( ) => {
2961+ // Test various scoring scenarios by creating mock union errors
2962+ const scenarioWithMissingFields = [
2963+ {
2964+ code : 'invalid_union' ,
2965+ unionErrors : [
2966+ {
2967+ // Variant with missing fields - should score highest
2968+ issues : [
2969+ { path : [ 'supports_moto' ] , message : 'Required' } ,
2970+ { path : [ 'merchant_label' ] , message : 'Required' } ,
2971+ ] ,
2972+ name : 'ZodError' ,
2973+ } ,
2974+ {
2975+ // Variant with only type errors - should score lower
2976+ issues : [
2977+ { path : [ 'type' ] , message : 'Expected string, received number' } ,
2978+ { path : [ 'id' ] , message : 'Expected number, received string' } ,
2979+ ] ,
2980+ name : 'ZodError' ,
2981+ } ,
2982+ {
2983+ // Variant with other errors - should score lowest
2984+ issues : [ { path : [ 'url' ] , message : 'Invalid URL format' } ] ,
2985+ name : 'ZodError' ,
2986+ } ,
2987+ ] ,
28462988 path : [ ] ,
28472989 message : 'Invalid input' ,
28482990 } ,
28492991 ]
2850- const expectedFormatted = outputContent `\n${ outputToken . errorText (
2851- 'Validation errors' ,
2852- ) } in tmp:\n\n${ parseHumanReadableError ( errorObject ) } `
2853- const abortOrReport = vi . fn ( )
2854- await parseConfigurationObject ( WebConfigurationSchema , 'tmp' , configurationObject , abortOrReport )
28552992
2856- expect ( abortOrReport ) . toHaveBeenCalledWith ( expectedFormatted , { } , 'tmp' )
2993+ const result = parseHumanReadableError ( scenarioWithMissingFields )
2994+
2995+ // Should show only the variant with missing fields (highest score)
2996+ expect ( result ) . toContain ( '[supports_moto]: Required' )
2997+ expect ( result ) . toContain ( '[merchant_label]: Required' )
2998+
2999+ // Should NOT show errors from other variants
3000+ expect ( result ) . not . toContain ( 'Expected string, received number' )
3001+ expect ( result ) . not . toContain ( 'Invalid URL format' )
3002+ expect ( result ) . not . toContain ( 'Union validation failed' )
3003+ } )
3004+
3005+ test ( 'parseHumanReadableError handles undefined messages gracefully' , ( ) => {
3006+ const undefinedMessageError = [
3007+ {
3008+ path : [ 'field' ] ,
3009+ message : undefined ,
3010+ } ,
3011+ {
3012+ path : [ ] ,
3013+ // message is completely missing
3014+ } ,
3015+ ]
3016+
3017+ const result = parseHumanReadableError ( undefinedMessageError )
3018+
3019+ expect ( result ) . toBe ( '• [field]: Unknown error\n• [root]: Unknown error\n' )
3020+ } )
3021+
3022+ test ( 'parseHumanReadableError handles mixed scoring scenarios' , ( ) => {
3023+ // Test scenario where we need to pick between variants with different error combinations
3024+ const mixedScenario = [
3025+ {
3026+ code : 'invalid_union' ,
3027+ unionErrors : [
3028+ {
3029+ // Mix of missing fields and type errors - this should win due to missing fields
3030+ issues : [
3031+ { path : [ 'required_field' ] , message : 'Required' } ,
3032+ { path : [ 'wrong_type' ] , message : 'Expected string, received number' } ,
3033+ ] ,
3034+ name : 'ZodError' ,
3035+ } ,
3036+ {
3037+ // Only type errors - should lose
3038+ issues : [
3039+ { path : [ 'field1' ] , message : 'Expected boolean, received string' } ,
3040+ { path : [ 'field2' ] , message : 'Expected array, received object' } ,
3041+ { path : [ 'field3' ] , message : 'Expected number, received string' } ,
3042+ ] ,
3043+ name : 'ZodError' ,
3044+ } ,
3045+ {
3046+ // Only other validation errors - should score lowest
3047+ issues : [
3048+ { path : [ 'url' ] , message : 'Must be valid URL' } ,
3049+ { path : [ 'email' ] , message : 'Must be valid email' } ,
3050+ ] ,
3051+ name : 'ZodError' ,
3052+ } ,
3053+ ] ,
3054+ path : [ ] ,
3055+ message : 'Invalid input' ,
3056+ } ,
3057+ ]
3058+
3059+ const result = parseHumanReadableError ( mixedScenario )
3060+
3061+ // Should pick the variant with missing field (even though it has fewer total errors)
3062+ expect ( result ) . toContain ( '[required_field]: Required' )
3063+ expect ( result ) . toContain ( '[wrong_type]: Expected string, received number' )
3064+
3065+ // Should not show errors from other variants
3066+ expect ( result ) . not . toContain ( 'Expected boolean, received string' )
3067+ expect ( result ) . not . toContain ( 'Must be valid URL' )
3068+ expect ( result ) . not . toContain ( 'Union validation failed' )
28573069 } )
28583070} )
28593071
0 commit comments