@@ -220,10 +220,11 @@ describe("Deployment happy path", async () => {
220220 chai . expect ( fsAccessStubs . getCall ( 2 ) . calledWith ( "lock-manager/deployment/pit/is-deployment-ready.sh" ) ) . be . true
221221 chai . expect ( fsAccessStubs . getCall ( 3 ) . calledWith ( "23456_t1/comp-1" ) ) . be . false
222222 chai . expect ( fsAccessStubs . getCall ( 3 ) . calledWith ( "./comp-1" ) ) . be . true
223- chai . expect ( fsAccessStubs . getCall ( 4 ) . calledWith ( "comp-1/deployment/pit/deploy.sh" ) ) . be . true
224- chai . expect ( fsAccessStubs . getCall ( 5 ) . calledWith ( "comp-1/deployment/pit/is-deployment-ready.sh" ) ) . be . true
225- chai . expect ( fsAccessStubs . getCall ( 6 ) . calledWith ( "./comp-1-test-app" ) ) . be . true
226- chai . expect ( fsAccessStubs . getCall ( 7 ) . calledWith ( "comp-1-test-app/deployment/pit/deploy.sh" ) ) . be . true
223+ // testApp deploys concurrently with comp-1, so its directory check interleaves
224+ chai . expect ( fsAccessStubs . getCall ( 4 ) . calledWith ( "./comp-1-test-app" ) ) . be . true
225+ chai . expect ( fsAccessStubs . getCall ( 5 ) . calledWith ( "comp-1/deployment/pit/deploy.sh" ) ) . be . true
226+ chai . expect ( fsAccessStubs . getCall ( 6 ) . calledWith ( "comp-1-test-app/deployment/pit/deploy.sh" ) ) . be . true
227+ chai . expect ( fsAccessStubs . getCall ( 7 ) . calledWith ( "comp-1/deployment/pit/is-deployment-ready.sh" ) ) . be . true
227228 chai . expect ( fsAccessStubs . getCall ( 8 ) . calledWith ( "comp-1-test-app/deployment/pit/is-deployment-ready.sh" ) ) . be . true
228229
229230 // check shell calls
@@ -258,25 +259,26 @@ describe("Deployment happy path", async () => {
258259 { homeDir : "lock-manager" } )
259260 ) . be . true
260261
262+ // testApp deploys concurrently with comp-1, so git log calls interleave
261263 chai . expect ( execStub . getCall ( 5 ) . calledWith ( `cd comp-1 && git log --pretty=format:"%h" -1` ) ) . be . true
262264
263- chai . expect ( execStub . getCall ( 6 ) . calledWith (
265+ chai . expect ( execStub . getCall ( 6 ) . calledWith ( `cd comp-1-test-app && git log --pretty=format:"%h" -1` ) ) . be . true
266+
267+ chai . expect ( execStub . getCall ( 7 ) . calledWith (
264268 "deployment/pit/deploy.sh nsChild" ,
265269 { homeDir : "comp-1" , logFileName : "12345_t1/logs/deploy-nsChild-comp-1.log" , tailTarget : sinon . match . any } )
266270 ) . be . true
267271
268- chai . expect ( execStub . getCall ( 7 ) . calledWith (
269- "deployment/pit/is-deployment-ready.sh nsChild" ,
270- { homeDir : "comp-1" }
271- ) ) . be . true
272-
273- chai . expect ( execStub . getCall ( 8 ) . calledWith ( `cd comp-1-test-app && git log --pretty=format:"%h" -1` ) ) . be . true
274-
275- chai . expect ( execStub . getCall ( 9 ) . calledWith (
272+ chai . expect ( execStub . getCall ( 8 ) . calledWith (
276273 "deployment/pit/deploy.sh nsChild t1" ,
277274 { homeDir : "comp-1-test-app" , logFileName : `12345_t1/logs/deploy-nsChild-comp-1-test-app.log` , tailTarget : sinon . match . any } )
278275 ) . be . true
279276
277+ chai . expect ( execStub . getCall ( 9 ) . calledWith (
278+ "deployment/pit/is-deployment-ready.sh nsChild" ,
279+ { homeDir : "comp-1" } )
280+ ) . be . true
281+
280282 chai . expect ( execStub . getCall ( 10 ) . calledWith (
281283 "deployment/pit/is-deployment-ready.sh nsChild" ,
282284 { homeDir : "comp-1-test-app" } )
@@ -435,10 +437,202 @@ describe("Deployment happy path", async () => {
435437 chai . expect ( fsAccessStubs . getCall ( 2 ) . calledWith ( "lock-manager/deployment/pit/is-deployment-ready.sh" ) ) . be . true
436438 chai . expect ( fsAccessStubs . getCall ( 3 ) . calledWith ( "23456_t2/comp-1" ) ) . be . false
437439 chai . expect ( fsAccessStubs . getCall ( 3 ) . calledWith ( "./comp-1" ) ) . be . true
438- chai . expect ( fsAccessStubs . getCall ( 4 ) . calledWith ( "./deployment/pit/deploy.sh" ) ) . be . true
439- chai . expect ( fsAccessStubs . getCall ( 5 ) . calledWith ( "./deployment/pit/is-deployment-ready.sh" ) ) . be . true
440- chai . expect ( fsAccessStubs . getCall ( 6 ) . calledWith ( "./comp-1-test-app" ) ) . be . true
441- chai . expect ( fsAccessStubs . getCall ( 7 ) . calledWith ( "comp-1-test-app/deployment/pit/deploy.sh" ) ) . be . true
440+ // testApp deploys concurrently with comp-1, so its directory check interleaves
441+ chai . expect ( fsAccessStubs . getCall ( 4 ) . calledWith ( "./comp-1-test-app" ) ) . be . true
442+ chai . expect ( fsAccessStubs . getCall ( 5 ) . calledWith ( "./deployment/pit/deploy.sh" ) ) . be . true
443+ chai . expect ( fsAccessStubs . getCall ( 6 ) . calledWith ( "comp-1-test-app/deployment/pit/deploy.sh" ) ) . be . true
444+ chai . expect ( fsAccessStubs . getCall ( 7 ) . calledWith ( "./deployment/pit/is-deployment-ready.sh" ) ) . be . true
442445 chai . expect ( fsAccessStubs . getCall ( 8 ) . calledWith ( "comp-1-test-app/deployment/pit/is-deployment-ready.sh" ) ) . be . true
443446 } )
444447} )
448+
449+ describe ( "deployGraph - deployment ordering and concurrency" , async ( ) => {
450+ const config : Config = {
451+ commitSha : "test-sha" ,
452+ workspace : "/tmp" ,
453+ clusterUrl : "http://localhost" ,
454+ parentNamespace : "ns" ,
455+ subNamespacePrefix : DEFAULT_SUB_NAMESPACE_PREFIX ,
456+ subNamespaceGeneratorType : SUB_NAMESPACE_GENERATOR_TYPE_DATE ,
457+ pitfile : "pitfile.yml" ,
458+ namespaceTimeoutSeconds : 2 ,
459+ report : { } ,
460+ targetEnv : "test" ,
461+ testStatusPollFrequencyMs : 100 ,
462+ testTimeoutMs : 1000 ,
463+ deployCheckFrequencyMs : 100 ,
464+ params : new Map ( ) ,
465+ useMockLockManager : false ,
466+ servicesAreExposedViaProxy : false ,
467+ lockManagerApiRetries : 1 ,
468+ enableCleanups : false ,
469+ testRunnerAppPort : 80
470+ }
471+ const namespace = "test-ns"
472+ const testSuiteId = "suite-1"
473+ const workspace = "test-workspace"
474+
475+ type Gate = { resolve : ( ) => void ; promise : Promise < void > }
476+ const makeGate = ( ) : Gate => {
477+ let resolve ! : ( ) => void
478+ const promise = new Promise < void > ( r => { resolve = r } )
479+ return { resolve, promise }
480+ }
481+
482+ const makeSpec = ( id : string , opts : { parallel ?: boolean ; dependsOn ?: string [ ] } = { } ) => ( {
483+ name : `${ id } -name` , id,
484+ location : { type : LocationType . Local } ,
485+ deploy : { command : `${ id } /deploy.sh` , statusCheck : { command : `${ id } /ready.sh` } } ,
486+ undeploy : { command : `${ id } /undeploy.sh` } ,
487+ ...opts
488+ } )
489+
490+ const loadWithStub = async ( deployStub : sinon . SinonStub ) =>
491+ esmock (
492+ "../src/test-suite-handler.js" ,
493+ { "../src/deployer.js" : { deployComponent : deployStub } } ,
494+ { "../src/logger.js" : { logger : { debug : ( ) => { } , info : ( ) => { } , warn : ( ) => { } , error : ( ) => { } } } }
495+ )
496+
497+ it ( "returns GraphDeploymentResult with all deployed components" , async ( ) => {
498+ const deployStub = sinon . stub ( ) . callsFake ( async ( _cfg , _ws , spec ) => `sha-${ spec . id } ` )
499+ const SuiteHandler = await loadWithStub ( deployStub )
500+ const graph = {
501+ testApp : makeSpec ( "test-app" ) ,
502+ components : [ makeSpec ( "comp-a" ) , makeSpec ( "comp-b" ) ]
503+ }
504+ const result = await SuiteHandler . deployGraph ( config , workspace , testSuiteId , graph , namespace )
505+ chai . expect ( result . components ) . to . have . length ( 2 )
506+ chai . expect ( result . components . map ( ( d : any ) => d . component . id ) ) . to . include . members ( [ "comp-a" , "comp-b" ] )
507+ chai . expect ( result . testApp . component . id ) . to . equal ( "test-app" )
508+ chai . expect ( result . testApp . commitSha ) . to . equal ( "sha-test-app" )
509+ } )
510+
511+ it ( "deploys parallel-flagged components concurrently" , async ( ) => {
512+ const gates : Record < string , Gate > = { B : makeGate ( ) , C : makeGate ( ) , testApp : makeGate ( ) }
513+ const started : string [ ] = [ ]
514+ const completed : string [ ] = [ ]
515+ const deployStub = sinon . stub ( ) . callsFake ( async ( _cfg , _ws , spec ) => {
516+ started . push ( spec . id )
517+ await gates [ spec . id ] . promise
518+ completed . push ( spec . id )
519+ return `sha-${ spec . id } `
520+ } )
521+ const SuiteHandler = await loadWithStub ( deployStub )
522+ const graph = {
523+ testApp : makeSpec ( "testApp" ) ,
524+ components : [ makeSpec ( "B" , { parallel : true } ) , makeSpec ( "C" , { parallel : true } ) ]
525+ }
526+ const deployPromise = SuiteHandler . deployGraph ( config , workspace , testSuiteId , graph , namespace )
527+ // Both B and C should have started before either completes
528+ chai . expect ( started ) . to . include ( "B" )
529+ chai . expect ( started ) . to . include ( "C" )
530+ chai . expect ( completed ) . to . not . include ( "B" )
531+ chai . expect ( completed ) . to . not . include ( "C" )
532+ gates [ "B" ] . resolve ( )
533+ gates [ "C" ] . resolve ( )
534+ gates [ "testApp" ] . resolve ( )
535+ await deployPromise
536+ chai . expect ( completed ) . to . include . members ( [ "B" , "C" ] )
537+ } )
538+
539+ it ( "deploys mixed parallel/sequential at same level concurrently" , async ( ) => {
540+ const gates : Record < string , Gate > = { B : makeGate ( ) , C : makeGate ( ) , testApp : makeGate ( ) }
541+ const started : string [ ] = [ ]
542+ const completed : string [ ] = [ ]
543+ const deployStub = sinon . stub ( ) . callsFake ( async ( _cfg , _ws , spec ) => {
544+ started . push ( spec . id )
545+ await gates [ spec . id ] . promise
546+ completed . push ( spec . id )
547+ return `sha-${ spec . id } `
548+ } )
549+ const SuiteHandler = await loadWithStub ( deployStub )
550+ // B is parallel:true, C has no parallel flag — same dependency level, so neither waits on the other
551+ const graph = {
552+ testApp : makeSpec ( "testApp" ) ,
553+ components : [ makeSpec ( "B" , { parallel : true } ) , makeSpec ( "C" ) ]
554+ }
555+ const deployPromise = SuiteHandler . deployGraph ( config , workspace , testSuiteId , graph , namespace )
556+ // B (parallelGroup) and C (sequentialGroup) fire their respective chains simultaneously —
557+ // both should be in-flight before either completes
558+ chai . expect ( started ) . to . include ( "B" )
559+ chai . expect ( started ) . to . include ( "C" )
560+ chai . expect ( completed ) . to . not . include ( "B" )
561+ chai . expect ( completed ) . to . not . include ( "C" )
562+ gates [ "B" ] . resolve ( )
563+ gates [ "C" ] . resolve ( )
564+ gates [ "testApp" ] . resolve ( )
565+ await deployPromise
566+ chai . expect ( completed ) . to . include . members ( [ "B" , "C" ] )
567+ } )
568+
569+ it ( "second dependency level does not start until first level completes" , async ( ) => {
570+ // Topology: A → B(parallel), A → C, B → D, C → D
571+ const gates : Record < string , Gate > = {
572+ A : makeGate ( ) , B : makeGate ( ) , C : makeGate ( ) , D : makeGate ( ) , testApp : makeGate ( )
573+ }
574+ const started : string [ ] = [ ]
575+ const deployStub = sinon . stub ( ) . callsFake ( async ( _cfg , _ws , spec ) => {
576+ started . push ( spec . id )
577+ await gates [ spec . id ] . promise
578+ return `sha-${ spec . id } `
579+ } )
580+ const SuiteHandler = await loadWithStub ( deployStub )
581+ const graph = {
582+ testApp : makeSpec ( "testApp" ) ,
583+ components : [
584+ makeSpec ( "A" ) ,
585+ makeSpec ( "B" , { parallel : true , dependsOn : [ "A" ] } ) ,
586+ makeSpec ( "C" , { dependsOn : [ "A" ] } ) ,
587+ makeSpec ( "D" , { dependsOn : [ "B" , "C" ] } )
588+ ]
589+ }
590+ const deployPromise = SuiteHandler . deployGraph ( config , workspace , testSuiteId , graph , namespace )
591+
592+ // Level 1: only A started; testApp is already in-flight concurrently
593+ chai . expect ( started ) . to . include ( "A" )
594+ chai . expect ( started ) . to . include ( "testApp" )
595+ chai . expect ( started ) . to . not . include ( "B" )
596+ chai . expect ( started ) . to . not . include ( "C" )
597+ chai . expect ( started ) . to . not . include ( "D" )
598+
599+ // Resolve A — level 2 (B and C) should start
600+ gates [ "A" ] . resolve ( )
601+ await new Promise ( r => setTimeout ( r , 0 ) )
602+ chai . expect ( started ) . to . include ( "B" )
603+ chai . expect ( started ) . to . include ( "C" )
604+ chai . expect ( started ) . to . not . include ( "D" )
605+
606+ // Resolve B and C — level 3 (D) should start
607+ gates [ "B" ] . resolve ( )
608+ gates [ "C" ] . resolve ( )
609+ await new Promise ( r => setTimeout ( r , 0 ) )
610+ chai . expect ( started ) . to . include ( "D" )
611+
612+ gates [ "D" ] . resolve ( )
613+ gates [ "testApp" ] . resolve ( )
614+ await deployPromise
615+ } )
616+
617+ it ( "deploys test app concurrently with the component chain" , async ( ) => {
618+ const gates : Record < string , Gate > = { A : makeGate ( ) , testApp : makeGate ( ) }
619+ const started : string [ ] = [ ]
620+ const deployStub = sinon . stub ( ) . callsFake ( async ( _cfg , _ws , spec ) => {
621+ started . push ( spec . id )
622+ await gates [ spec . id ] . promise
623+ return `sha-${ spec . id } `
624+ } )
625+ const SuiteHandler = await loadWithStub ( deployStub )
626+ const graph = {
627+ testApp : makeSpec ( "testApp" ) ,
628+ components : [ makeSpec ( "A" ) ]
629+ }
630+ const deployPromise = SuiteHandler . deployGraph ( config , workspace , testSuiteId , graph , namespace )
631+ // A and testApp should both be in-flight before either completes
632+ chai . expect ( started ) . to . include ( "A" )
633+ chai . expect ( started ) . to . include ( "testApp" )
634+ gates [ "A" ] . resolve ( )
635+ gates [ "testApp" ] . resolve ( )
636+ await deployPromise
637+ } )
638+ } )
0 commit comments