@@ -23,10 +23,12 @@ import {
2323 schemaMock ,
2424} from '@sim/testing'
2525import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
26+ import { workflowStateSchema } from '@/lib/api/contracts/workflows'
2627import type {
2728 BlockState as AppBlockState ,
2829 WorkflowState as AppWorkflowState ,
2930} from '@/stores/workflows/workflow/types'
31+ import { generateLoopBlocks } from '@/stores/workflows/workflow/utils'
3032
3133/**
3234 * Type helper for converting test workflow state to app workflow state.
@@ -348,6 +350,110 @@ describe('Database Helpers', () => {
348350 } )
349351
350352 describe ( 'loadWorkflowFromNormalizedTables' , ( ) => {
353+ it . each ( [ 'for' , 'forEach' , 'while' , 'doWhile' ] as const ) (
354+ 'preserves valid block counts and expressions for %s loops even when subflow counts differ' ,
355+ async ( loopType ) => {
356+ const data = {
357+ count : 9 ,
358+ loopType,
359+ collection : '<source.items>' ,
360+ whileCondition : '<source.hasMore>' ,
361+ doWhileCondition : '<source.hasMore>' ,
362+ width : 600 ,
363+ parentId : 'outer-loop' ,
364+ extent : 'parent' as const ,
365+ }
366+ queueLoadFixtures ( {
367+ blocks : [ { ...toDbBlock ( createLoopBlock ( { id : 'loop-1' } ) , mockWorkflowId ) , data } ] ,
368+ subflows : [
369+ {
370+ id : 'loop-1' ,
371+ type : 'loop' ,
372+ config : {
373+ nodes : [ ] ,
374+ loopType,
375+ iterations : 3 ,
376+ forEachItems : data . collection ,
377+ whileCondition : data . whileCondition ,
378+ doWhileCondition : data . doWhileCondition ,
379+ } ,
380+ } ,
381+ ] ,
382+ } )
383+
384+ const loaded = await dbHelpers . loadWorkflowFromNormalizedTables ( mockWorkflowId )
385+ const parsed = workflowStateSchema . parse ( loaded )
386+
387+ expect ( parsed . blocks [ 'loop-1' ] . data ) . toEqual ( data )
388+ expect ( parsed . loops ?. [ 'loop-1' ] . iterations ) . toBe ( 3 )
389+ expect ( generateLoopBlocks ( loaded ! . blocks ) [ 'loop-1' ] ) . toMatchObject ( {
390+ iterations : 9 ,
391+ loopType,
392+ forEachItems : data . collection ,
393+ whileCondition : data . whileCondition ,
394+ doWhileCondition : data . doWhileCondition ,
395+ } )
396+ expect ( dbChainMockFns . update ) . not . toHaveBeenCalled ( )
397+ }
398+ )
399+
400+ it ( 'keeps an absent block count absent so serialization retains its existing default' , async ( ) => {
401+ queueLoadFixtures ( {
402+ blocks : [
403+ {
404+ ...toDbBlock ( createLoopBlock ( { id : 'loop-1' } ) , mockWorkflowId ) ,
405+ data : { loopType : 'for' } ,
406+ } ,
407+ ] ,
408+ subflows : [
409+ { id : 'loop-1' , type : 'loop' , config : { nodes : [ ] , loopType : 'for' , iterations : 3 } } ,
410+ ] ,
411+ } )
412+
413+ const loaded = await dbHelpers . loadWorkflowFromNormalizedTables ( mockWorkflowId )
414+
415+ expect ( loaded ?. blocks [ 'loop-1' ] . data ?. count ) . toBeUndefined ( )
416+ expect ( loaded ?. loops [ 'loop-1' ] . iterations ) . toBe ( 3 )
417+ expect ( generateLoopBlocks ( loaded ! . blocks ) [ 'loop-1' ] . iterations ) . toBe ( 5 )
418+ expect ( dbChainMockFns . update ) . not . toHaveBeenCalled ( )
419+ } )
420+
421+ it ( 'serves a legacy forEach loop with a string count through the workflow read contract' , async ( ) => {
422+ const collection = '<source.items>'
423+ const loopRow = toDbBlock ( createLoopBlock ( { id : 'loop-1' } ) , mockWorkflowId )
424+ queueLoadFixtures ( {
425+ blocks : [
426+ {
427+ ...loopRow ,
428+ data : { ...loopRow . data , loopType : 'forEach' , count : collection , collection } ,
429+ } ,
430+ ] ,
431+ subflows : [
432+ {
433+ id : 'loop-1' ,
434+ type : 'loop' ,
435+ config : {
436+ nodes : [ ] ,
437+ loopType : 'forEach' ,
438+ iterations : collection ,
439+ forEachItems : collection ,
440+ } ,
441+ } ,
442+ ] ,
443+ } )
444+
445+ const loaded = await dbHelpers . loadWorkflowFromNormalizedTables ( mockWorkflowId )
446+ const parsed = workflowStateSchema . parse ( loaded )
447+
448+ expect ( parsed . blocks [ 'loop-1' ] . data ) . toMatchObject ( { count : 1 , collection } )
449+ expect ( parsed . loops ?. [ 'loop-1' ] ) . toMatchObject ( {
450+ loopType : 'forEach' ,
451+ iterations : 1 ,
452+ forEachItems : collection ,
453+ } )
454+ expect ( dbChainMockFns . update ) . not . toHaveBeenCalled ( )
455+ } )
456+
351457 it ( 'should successfully load workflow data from normalized tables' , async ( ) => {
352458 queueLoadFixtures ( {
353459 blocks : mockBlocksFromDb ,
@@ -401,6 +507,7 @@ describe('Database Helpers', () => {
401507 whileCondition : '' ,
402508 enabled : true ,
403509 } )
510+ expect ( result ?. blocks [ 'loop-1' ] . data ?. count ) . toBe ( 3 )
404511
405512 expect ( result ?. parallels [ 'parallel-1' ] ) . toEqual ( {
406513 id : 'parallel-1' ,
0 commit comments