@@ -112,13 +112,16 @@ describe('ExtractFlowOutput utility type', () => {
112112 . step ( { slug : 'step3' , dependsOn : [ 'step1' ] } , ( deps ) => ( {
113113 value : deps . step1 . value - 1 ,
114114 } ) )
115- . step ( { slug : 'step4' , dependsOn : [ 'step2' , 'step3' ] } , async ( deps , ctx ) => {
116- const flowInput = await ctx . flowInput ;
117- return {
118- sum : deps . step2 . value + deps . step3 . value ,
119- original : flowInput . input ,
120- } ;
121- } ) ;
115+ . step (
116+ { slug : 'step4' , dependsOn : [ 'step2' , 'step3' ] } ,
117+ async ( deps , ctx ) => {
118+ const flowInput = await ctx . flowInput ;
119+ return {
120+ sum : deps . step2 . value + deps . step3 . value ,
121+ original : flowInput . input ,
122+ } ;
123+ }
124+ ) ;
122125
123126 type FlowOutput = ExtractFlowOutput < typeof complexFlow > ;
124127
@@ -138,4 +141,122 @@ describe('ExtractFlowOutput utility type', () => {
138141 step3 : unknown ;
139142 } > ( ) ;
140143 } ) ;
144+
145+ it ( 'makes skippable leaf steps optional in flow output' , ( ) => {
146+ const skippableLeafFlow = new Flow < { input : string } > ( {
147+ slug : 'skippable_leaf_flow' ,
148+ } )
149+ . step ( { slug : 'prepare' } , ( flowInput ) => ( { text : flowInput . input } ) )
150+ . step (
151+ {
152+ slug : 'leaf_optional' ,
153+ dependsOn : [ 'prepare' ] ,
154+ if : { prepare : { text : 'run' } } ,
155+ whenUnmet : 'skip' ,
156+ } ,
157+ ( deps ) => ( { value : deps . prepare . text . toUpperCase ( ) } )
158+ ) ;
159+
160+ type FlowOutput = ExtractFlowOutput < typeof skippableLeafFlow > ;
161+
162+ expectTypeOf < FlowOutput > ( ) . toMatchTypeOf < {
163+ leaf_optional ?: StepOutput < typeof skippableLeafFlow , 'leaf_optional' > ;
164+ } > ( ) ;
165+
166+ expectTypeOf < {
167+ leaf_optional ?: StepOutput < typeof skippableLeafFlow , 'leaf_optional' > ;
168+ } > ( ) . toMatchTypeOf < FlowOutput > ( ) ;
169+ } ) ;
170+
171+ it ( 'keeps non-skippable leaf steps required in flow output' , ( ) => {
172+ const requiredLeafFlow = new Flow < { input : string } > ( {
173+ slug : 'required_leaf_flow' ,
174+ } ) . step ( { slug : 'leaf_required' } , ( flowInput ) => ( {
175+ value : flowInput . input . length ,
176+ } ) ) ;
177+
178+ type FlowOutput = ExtractFlowOutput < typeof requiredLeafFlow > ;
179+
180+ expectTypeOf < FlowOutput > ( ) . toMatchTypeOf < {
181+ leaf_required : StepOutput < typeof requiredLeafFlow , 'leaf_required' > ;
182+ } > ( ) ;
183+
184+ expectTypeOf < {
185+ leaf_required : StepOutput < typeof requiredLeafFlow , 'leaf_required' > ;
186+ } > ( ) . toMatchTypeOf < FlowOutput > ( ) ;
187+ } ) ;
188+
189+ it ( 'supports mixed required and skippable leaf outputs' , ( ) => {
190+ const mixedLeafFlow = new Flow < { input : string } > ( {
191+ slug : 'mixed_leaf_flow' ,
192+ } )
193+ . step ( { slug : 'prepare' } , ( flowInput ) => ( { text : flowInput . input } ) )
194+ . step ( { slug : 'required_leaf' , dependsOn : [ 'prepare' ] } , ( deps ) => ( {
195+ value : deps . prepare . text . length ,
196+ } ) )
197+ . step (
198+ {
199+ slug : 'optional_leaf' ,
200+ dependsOn : [ 'prepare' ] ,
201+ if : { prepare : { text : 'run' } } ,
202+ whenUnmet : 'skip-cascade' ,
203+ } ,
204+ ( deps ) => ( { value : deps . prepare . text . toUpperCase ( ) } )
205+ ) ;
206+
207+ type FlowOutput = ExtractFlowOutput < typeof mixedLeafFlow > ;
208+
209+ expectTypeOf < FlowOutput > ( ) . toMatchTypeOf < {
210+ required_leaf : StepOutput < typeof mixedLeafFlow , 'required_leaf' > ;
211+ optional_leaf ?: StepOutput < typeof mixedLeafFlow , 'optional_leaf' > ;
212+ } > ( ) ;
213+
214+ expectTypeOf < {
215+ required_leaf : StepOutput < typeof mixedLeafFlow , 'required_leaf' > ;
216+ optional_leaf ?: StepOutput < typeof mixedLeafFlow , 'optional_leaf' > ;
217+ } > ( ) . toMatchTypeOf < FlowOutput > ( ) ;
218+ } ) ;
219+
220+ it ( 'preserves optional inner keys for required leaf outputs' , ( ) => {
221+ const requiredOptionalInnerFlow = new Flow < { input : string } > ( {
222+ slug : 'required_optional_inner_flow' ,
223+ } ) . step ( { slug : 'leaf_required' } , ( ) : { entryId ?: string } =>
224+ Math . random ( ) > 0.5 ? { entryId : 'entry-1' } : { }
225+ ) ;
226+
227+ type FlowOutput = ExtractFlowOutput < typeof requiredOptionalInnerFlow > ;
228+
229+ expectTypeOf < FlowOutput > ( ) . toMatchTypeOf < {
230+ leaf_required : { entryId ?: string } ;
231+ } > ( ) ;
232+ expectTypeOf < {
233+ leaf_required : { entryId ?: string } ;
234+ } > ( ) . toMatchTypeOf < FlowOutput > ( ) ;
235+ } ) ;
236+
237+ it ( 'supports optional outer key with optional inner keys for skippable leaves' , ( ) => {
238+ const skippableOptionalInnerFlow = new Flow < { input : string } > ( {
239+ slug : 'skippable_optional_inner_flow' ,
240+ } )
241+ . step ( { slug : 'prepare' } , ( flowInput ) => ( { text : flowInput . input } ) )
242+ . step (
243+ {
244+ slug : 'leaf_optional' ,
245+ dependsOn : [ 'prepare' ] ,
246+ if : { prepare : { text : 'run' } } ,
247+ whenUnmet : 'skip' ,
248+ } ,
249+ ( ) : { meta ?: { tag : string } } =>
250+ Math . random ( ) > 0.5 ? { meta : { tag : 'ok' } } : { }
251+ ) ;
252+
253+ type FlowOutput = ExtractFlowOutput < typeof skippableOptionalInnerFlow > ;
254+
255+ expectTypeOf < FlowOutput > ( ) . toMatchTypeOf < {
256+ leaf_optional ?: { meta ?: { tag : string } } ;
257+ } > ( ) ;
258+ expectTypeOf < {
259+ leaf_optional ?: { meta ?: { tag : string } } ;
260+ } > ( ) . toMatchTypeOf < FlowOutput > ( ) ;
261+ } ) ;
141262} ) ;
0 commit comments