@@ -79,6 +79,168 @@ describe("Component Dependency Tests", () => {
7979 } )
8080 } )
8181
82+ describe ( "Parallel Deploy Flag" , ( ) => {
83+ it ( "should load and sort components with parallel deploy flag set" , async ( ) => {
84+ const pitfilePath = path . join ( __dirname , "pitfile" , "test-pitfile-valid-with-parallel-deploy.yml" )
85+ const pitfile = await PifFileLoader . loadFromFile ( pitfilePath )
86+ const testSuite = pitfile . testSuites [ 0 ]
87+ const components = testSuite . deployment . graph . components
88+
89+ // Should validate successfully
90+ expect ( ( ) => validateDependencies ( components , testSuite . name ) ) . not . to . throw ( )
91+
92+ const sortResult = topologicalSort ( components )
93+
94+ // Level 0: no dependencies — database, message-queue, config-service, metrics-collector
95+ expect ( sortResult . levels [ 0 ] . map ( c => c . id ) ) . to . deep . equal ( [
96+ "database" , "message-queue" , "config-service" , "metrics-collector"
97+ ] )
98+
99+ // Level 1: api-service (depends on database + message-queue), cache-service (depends on database)
100+ expect ( sortResult . levels [ 1 ] . map ( c => c . id ) ) . to . deep . equal ( [ "api-service" , "cache-service" ] )
101+
102+ // Level 2: backend-for-frontend (depends on api-service + cache-service)
103+ expect ( sortResult . levels [ 2 ] . map ( c => c . id ) ) . to . deep . equal ( [ "backend-for-frontend" ] )
104+
105+ // Level 3: frontend (depends on backend-for-frontend)
106+ expect ( sortResult . levels [ 3 ] . map ( c => c . id ) ) . to . deep . equal ( [ "frontend" ] )
107+
108+ // Parallel flags are preserved through sorting
109+ const db = components . find ( c => c . id === "database" ) !
110+ const configService = components . find ( c => c . id === "config-service" ) !
111+ const apiService = components . find ( c => c . id === "api-service" ) !
112+ const cacheService = components . find ( c => c . id === "cache-service" ) !
113+
114+ expect ( db . deploy . parallel ) . to . be . true
115+ expect ( apiService . deploy . parallel ) . to . be . true
116+ expect ( configService . deploy . parallel ) . to . be . undefined // sequential
117+ expect ( cacheService . deploy . parallel ) . to . be . undefined // sequential, despite depending on a parallel component
118+ } )
119+
120+ it ( "testApp parallel flag is independent of component parallel flags" , async ( ) => {
121+ const pitfilePath = path . join ( __dirname , "pitfile" , "test-pitfile-valid-with-parallel-deploy.yml" )
122+ const pitfile = await PifFileLoader . loadFromFile ( pitfilePath )
123+ const testSuite = pitfile . testSuites [ 0 ]
124+
125+ expect ( testSuite . deployment . graph . testApp . deploy . parallel ) . to . be . true
126+ } )
127+
128+ it ( "multi-stage graph: mixed parallel/sequential at every level, cross-stage ancestry, fan-in convergence" , async ( ) => {
129+ const pitfilePath = path . join ( __dirname , "pitfile" , "test-pitfile-valid-with-parallel-subgroups.yml" )
130+ const pitfile = await PifFileLoader . loadFromFile ( pitfilePath )
131+ // Suite index 1: multi-stage-mixed
132+ const testSuite = pitfile . testSuites [ 1 ]
133+ const components = testSuite . deployment . graph . components
134+
135+ expect ( ( ) => validateDependencies ( components , testSuite . name ) ) . not . to . throw ( )
136+
137+ const sortResult = topologicalSort ( components )
138+ expect ( sortResult . levels ) . to . have . length ( 5 )
139+
140+ // Stage 0: two independent roots — Infra (parallel) and Config (sequential)
141+ expect ( sortResult . levels [ 0 ] . map ( c => c . id ) ) . to . deep . equal ( [ "infra" , "config" ] )
142+ expect ( components . find ( c => c . id === "infra" ) ! . deploy . parallel ) . to . be . true
143+ expect ( components . find ( c => c . id === "config" ) ! . deploy . parallel ) . to . be . undefined
144+
145+ // Stage 1: Auth🔀, Cache🔀 depend only on Infra; Registry depends on both roots (sequential)
146+ expect ( sortResult . levels [ 1 ] . map ( c => c . id ) ) . to . deep . equal ( [ "auth" , "cache" , "registry" ] )
147+ expect ( components . find ( c => c . id === "auth" ) ! . deploy . parallel ) . to . be . true
148+ expect ( components . find ( c => c . id === "cache" ) ! . deploy . parallel ) . to . be . true
149+ expect ( components . find ( c => c . id === "registry" ) ! . deploy . parallel ) . to . be . undefined
150+ // Registry's cross-stage ancestry: depends on both stage-0 nodes
151+ expect ( components . find ( c => c . id === "registry" ) ! . dependsOn ) . to . deep . equal ( [ "infra" , "config" ] )
152+
153+ // Stage 2: API🔀 (Auth+Cache), Worker🔀 (Cache+Registry — one parallel, one sequential parent), Scheduler (Registry)
154+ expect ( sortResult . levels [ 2 ] . map ( c => c . id ) ) . to . deep . equal ( [ "api" , "worker" , "scheduler" ] )
155+ expect ( components . find ( c => c . id === "api" ) ! . deploy . parallel ) . to . be . true
156+ expect ( components . find ( c => c . id === "worker" ) ! . deploy . parallel ) . to . be . true
157+ expect ( components . find ( c => c . id === "scheduler" ) ! . deploy . parallel ) . to . be . undefined
158+ expect ( components . find ( c => c . id === "worker" ) ! . dependsOn ) . to . deep . equal ( [ "cache" , "registry" ] )
159+
160+ // Stage 3: Gateway — sequential fan-in from all three stage-2 nodes
161+ expect ( sortResult . levels [ 3 ] . map ( c => c . id ) ) . to . deep . equal ( [ "gateway" ] )
162+ expect ( components . find ( c => c . id === "gateway" ) ! . deploy . parallel ) . to . be . undefined
163+ expect ( components . find ( c => c . id === "gateway" ) ! . dependsOn ) . to . deep . equal ( [ "api" , "worker" , "scheduler" ] )
164+
165+ // Stage 4: Frontend🔀 and Admin🔀 both depend on Gateway
166+ expect ( sortResult . levels [ 4 ] . map ( c => c . id ) ) . to . deep . equal ( [ "frontend" , "admin" ] )
167+ expect ( components . find ( c => c . id === "frontend" ) ! . deploy . parallel ) . to . be . true
168+ expect ( components . find ( c => c . id === "admin" ) ! . deploy . parallel ) . to . be . true
169+
170+ // Undeployment: reverse stages, within each stage order is preserved
171+ const undeployOrder = reverseTopologicalSort ( sortResult ) . map ( c => c . id )
172+ expect ( undeployOrder ) . to . deep . equal ( [
173+ "frontend" , "admin" ,
174+ "gateway" ,
175+ "api" , "worker" , "scheduler" ,
176+ "auth" , "cache" , "registry" ,
177+ "infra" , "config"
178+ ] )
179+
180+ // testApp is marked parallel (runs concurrently with component stages)
181+ expect ( testSuite . deployment . graph . testApp . deploy . parallel ) . to . be . true
182+ } )
183+
184+ it ( "A -> [B🔀, C🔀, D🔀] -> E: all middle components parallel, flanked by sequential nodes" , async ( ) => {
185+ const pitfilePath = path . join ( __dirname , "pitfile" , "test-pitfile-valid-with-parallel-subgroups.yml" )
186+ const pitfile = await PifFileLoader . loadFromFile ( pitfilePath )
187+ // Suite index 0: all-parallel-middle
188+ const testSuite = pitfile . testSuites [ 0 ]
189+ const components = testSuite . deployment . graph . components
190+
191+ expect ( ( ) => validateDependencies ( components , testSuite . name ) ) . not . to . throw ( )
192+
193+ const sortResult = topologicalSort ( components )
194+
195+ // Level 0: A (no dependencies, sequential)
196+ expect ( sortResult . levels [ 0 ] . map ( c => c . id ) ) . to . deep . equal ( [ "node-a" ] )
197+ expect ( components . find ( c => c . id === "node-a" ) ! . deploy . parallel ) . to . be . undefined
198+
199+ // Level 1: B, C, D (all depend on A, all parallel)
200+ expect ( sortResult . levels [ 1 ] . map ( c => c . id ) ) . to . deep . equal ( [ "node-b" , "node-c" , "node-d" ] )
201+ expect ( components . find ( c => c . id === "node-b" ) ! . deploy . parallel ) . to . be . true
202+ expect ( components . find ( c => c . id === "node-c" ) ! . deploy . parallel ) . to . be . true
203+ expect ( components . find ( c => c . id === "node-d" ) ! . deploy . parallel ) . to . be . true
204+
205+ // Level 2: E (depends on B, C, D — sequential)
206+ expect ( sortResult . levels [ 2 ] . map ( c => c . id ) ) . to . deep . equal ( [ "node-e" ] )
207+ expect ( components . find ( c => c . id === "node-e" ) ! . deploy . parallel ) . to . be . undefined
208+
209+ // Undeployment reverses: E -> B,C,D -> A
210+ const undeployOrder = reverseTopologicalSort ( sortResult ) . map ( c => c . id )
211+ expect ( undeployOrder ) . to . deep . equal ( [ "node-e" , "node-b" , "node-c" , "node-d" , "node-a" ] )
212+ } )
213+
214+ it ( "A -> [B🔀, C🔀, D] -> E: mixed parallel/sequential middle layer, E depends on all three" , async ( ) => {
215+ const pitfilePath = path . join ( __dirname , "pitfile" , "test-pitfile-valid-with-parallel-subgroups.yml" )
216+ const pitfile = await PifFileLoader . loadFromFile ( pitfilePath )
217+ // Suite index 2: mixed-parallel-middle
218+ const testSuite = pitfile . testSuites [ 2 ]
219+ const components = testSuite . deployment . graph . components
220+
221+ expect ( ( ) => validateDependencies ( components , testSuite . name ) ) . not . to . throw ( )
222+
223+ const sortResult = topologicalSort ( components )
224+
225+ // Level 0: A
226+ expect ( sortResult . levels [ 0 ] . map ( c => c . id ) ) . to . deep . equal ( [ "node-a" ] )
227+
228+ // Level 1: B, C, D — same dependency level; B and C parallel, D sequential
229+ expect ( sortResult . levels [ 1 ] . map ( c => c . id ) ) . to . deep . equal ( [ "node-b" , "node-c" , "node-d" ] )
230+ expect ( components . find ( c => c . id === "node-b" ) ! . deploy . parallel ) . to . be . true
231+ expect ( components . find ( c => c . id === "node-c" ) ! . deploy . parallel ) . to . be . true
232+ expect ( components . find ( c => c . id === "node-d" ) ! . deploy . parallel ) . to . be . undefined // sequential
233+
234+ // Level 2: E (waits for all of B, C and D)
235+ expect ( sortResult . levels [ 2 ] . map ( c => c . id ) ) . to . deep . equal ( [ "node-e" ] )
236+ expect ( components . find ( c => c . id === "node-e" ) ! . deploy . parallel ) . to . be . undefined
237+
238+ // Undeployment reverses: E -> B,C,D -> A
239+ const undeployOrder = reverseTopologicalSort ( sortResult ) . map ( c => c . id )
240+ expect ( undeployOrder ) . to . deep . equal ( [ "node-e" , "node-b" , "node-c" , "node-d" , "node-a" ] )
241+ } )
242+ } )
243+
82244 describe ( "Complex Dependency Scenarios" , ( ) => {
83245 it ( "should handle complex dependency graphs correctly" , async ( ) => {
84246 const pitfilePath = path . join ( __dirname , "pitfile" , "test-pitfile-valid-with-complex-dependencies.yml" )
0 commit comments