@@ -372,10 +372,43 @@ describe("FsProjectManager.deploy", () => {
372372 return join ( directory , "example" , "agentcore" , "cdk" , "cdk.out" ) ;
373373 }
374374
375+ // How the generated CDK app names the stack it synthesizes for a target.
376+ function stackName ( target : string ) : string {
377+ return `AgentCore-example-${ target } ` ;
378+ }
379+
380+ // Stands in for what synth leaves behind: a manifest with one stack per target,
381+ // each tagged with the target it belongs to. deploy reads it to find the stack to
382+ // ship, and the stubbed runner never writes one.
383+ async function synthesized ( directory : string , targetNames : string [ ] ) : Promise < void > {
384+ const assembly = assemblyDirectory ( directory ) ;
385+ await mkdir ( assembly , { recursive : true } ) ;
386+ await writeFile (
387+ join ( assembly , "manifest.json" ) ,
388+ JSON . stringify ( {
389+ version : "36.0.0" ,
390+ artifacts : {
391+ // A non-stack artifact, as a real assembly has: only stacks are candidates.
392+ Tree : { type : "cdk:tree" } ,
393+ ...Object . fromEntries (
394+ targetNames . map ( ( target ) => [
395+ stackName ( target ) ,
396+ {
397+ type : "aws:cloudformation:stack" ,
398+ properties : { tags : { "agentcore:target-name" : target } } ,
399+ } ,
400+ ] ) ,
401+ ) ,
402+ } ,
403+ } ) ,
404+ ) ;
405+ }
406+
375407 // deploy() builds first, so the CDK app's node_modules must exist; create() with
376408 // skipInstall never produces them. Targets overwrite the empty list create()
377409 // scaffolds; null leaves that empty list in place, and a string is written verbatim
378- // so a test can supply invalid JSON.
410+ // so a test can supply invalid JSON. A well-formed list also gets the assembly
411+ // synth would have produced for it.
379412 async function scaffolded (
380413 subject : FsProjectManager ,
381414 directory : string ,
@@ -396,6 +429,12 @@ describe("FsProjectManager.deploy", () => {
396429 typeof targets === "string" ? targets : JSON . stringify ( targets ) ,
397430 ) ;
398431 }
432+ if ( Array . isArray ( targets ) ) {
433+ await synthesized (
434+ directory ,
435+ ( targets as { name ?: string } [ ] ) . flatMap ( ( { name } ) => ( name ? [ name ] : [ ] ) ) ,
436+ ) ;
437+ }
399438 return project ;
400439 }
401440
@@ -405,26 +444,28 @@ describe("FsProjectManager.deploy", () => {
405444 return events ;
406445 }
407446
408- test ( "synthesizes, bootstraps the target environment, then deploys every stack" , async ( ) => {
447+ test ( "synthesizes, bootstraps the target environment, then deploys its stack" , async ( ) => {
409448 const directory = await inTempDirectory ( ) ;
410449 const { manager : subject , commands, runs } = deployManager ( ) ;
411450 const project = await scaffolded ( subject , directory ) ;
412451 commands . length = 0 ; // discard create()'s commands
413452 const cdkDir = join ( directory , "example" , "agentcore" , "cdk" ) ;
414453 const options = { assemblyDirectory : assemblyDirectory ( directory ) , region : REGION } ;
415454
416- const events = await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false } ) ) ;
455+ const events = await drain (
456+ subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "default" } ) ,
457+ ) ;
417458
418459 // Only synthesis shells out; everything that reaches AWS goes through the toolkit.
419460 expect ( commands ) . toEqual ( [ { command : synthCommand ( directory ) , cwd : cdkDir } ] ) ;
420461 expect ( runs ) . toEqual ( [
421462 { operation : { kind : "bootstrap" , environments : [ "aws://111122223333/us-east-1" ] } , options } ,
422- { operation : { kind : "deploy" } , options } ,
463+ { operation : { kind : "deploy" , stackName : stackName ( "default" ) } , options } ,
423464 ] ) ;
424465 expect ( events ) . toEqual ( [
425466 { message : "Synthesizing CloudFormation templates" } ,
426467 { message : "Bootstrapping aws://111122223333/us-east-1" } ,
427- { message : " Deploying stacks" } ,
468+ { message : ` Deploying ${ stackName ( "default" ) } ` } ,
428469 ] ) ;
429470 } ) ;
430471
@@ -434,7 +475,9 @@ describe("FsProjectManager.deploy", () => {
434475 const project = await scaffolded ( subject , directory ) ;
435476 commands . length = 0 ;
436477
437- await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : true } ) ) ;
478+ await drain (
479+ subject . deploy ( project , { region : REGION , skipBootstrap : true , target : "default" } ) ,
480+ ) ;
438481
439482 // The invariant behind passing --output at all: whatever synth was told to write
440483 // is exactly what the toolkit is handed. Left to cdk.json's `output`, synth could
@@ -444,22 +487,71 @@ describe("FsProjectManager.deploy", () => {
444487 expect ( runs . map ( ( { options } ) => options . assemblyDirectory ) ) . toEqual ( [ assembly ] ) ;
445488 } ) ;
446489
447- test ( "bootstraps each distinct environment once, however many targets share it" , async ( ) => {
490+ // The three-target project the target-selection tests share.
491+ const TARGETS = [
492+ { name : "staging" , account : "111122223333" , region : "us-east-1" } ,
493+ { name : "prod" , account : "444455556666" , region : "eu-west-1" } ,
494+ { name : "default" , account : "777788889999" , region : "us-west-2" } ,
495+ ] ;
496+
497+ test ( "deploys only the requested target's stack, into only its environment" , async ( ) => {
448498 const directory = await inTempDirectory ( ) ;
449499 const { manager : subject , runs } = deployManager ( ) ;
450- const project = await scaffolded ( subject , directory , [
451- { name : "alpha" , account : "111122223333" , region : "us-east-1" } ,
452- { name : "beta" , account : "111122223333" , region : "us-east-1" } , // same environment
453- { name : "gamma" , account : "444455556666" , region : "eu-west-1" } ,
500+ const project = await scaffolded ( subject , directory , TARGETS ) ;
501+
502+ await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "prod" } ) ) ;
503+
504+ // A project with a staging and a prod target cannot reach the others by
505+ // accident: one deploy bootstraps one environment and ships one stack.
506+ expect ( runs . map ( ( { operation } ) => operation ) ) . toEqual ( [
507+ { kind : "bootstrap" , environments : [ "aws://444455556666/eu-west-1" ] } ,
508+ { kind : "deploy" , stackName : stackName ( "prod" ) } ,
509+ ] ) ;
510+ } ) ;
511+
512+ test ( "deploys the target named 'default' when none is requested" , async ( ) => {
513+ const directory = await inTempDirectory ( ) ;
514+ const { manager : subject , runs } = deployManager ( ) ;
515+ const project = await scaffolded ( subject , directory , TARGETS ) ;
516+
517+ // What the handler passes when --target is omitted, and the name the example in
518+ // the empty-targets error uses.
519+ await drain (
520+ subject . deploy ( project , { region : REGION , skipBootstrap : true , target : "default" } ) ,
521+ ) ;
522+
523+ expect ( runs . map ( ( { operation } ) => operation ) ) . toEqual ( [
524+ { kind : "deploy" , stackName : stackName ( "default" ) } ,
454525 ] ) ;
526+ } ) ;
527+
528+ test ( "names the configured targets when the requested one is not among them" , async ( ) => {
529+ const directory = await inTempDirectory ( ) ;
530+ const { manager : subject , commands, runs } = deployManager ( ) ;
531+ const project = await scaffolded ( subject , directory , TARGETS ) ;
532+ commands . length = 0 ;
533+
534+ await expect (
535+ drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "prd" } ) ) ,
536+ ) . rejects . toThrow ( / n o d e p l o y m e n t t a r g e t n a m e d ' p r d ' .* s t a g i n g , p r o d , d e f a u l t / s) ;
537+ // Resolved before synthesizing, so a misspelled --target costs no build.
538+ expect ( commands ) . toEqual ( [ ] ) ;
539+ expect ( runs ) . toEqual ( [ ] ) ;
540+ } ) ;
455541
456- await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false } ) ) ;
542+ test ( "fails when the synthesized assembly has no stack for the target" , async ( ) => {
543+ const directory = await inTempDirectory ( ) ;
544+ const { manager : subject , runs } = deployManager ( ) ;
545+ const project = await scaffolded ( subject , directory ) ;
546+ // An assembly synthesized from a different target list than the one on disk —
547+ // what a hand-edited CDK app that stops tagging its stacks would leave behind.
548+ await synthesized ( directory , [ "other" ] ) ;
457549
458- expect (
459- runs . flatMap ( ( { operation } ) =>
460- operation . kind === "bootstrap" ? operation . environments : [ ] ,
461- ) ,
462- ) . toEqual ( [ "aws://111122223333/us-east-1" , "aws://444455556666/eu-west-1" ] ) ;
550+ await expect (
551+ drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "default" } ) ) ,
552+ ) . rejects . toThrow ( / n o s t a c k f o r d e p l o y m e n t t a r g e t ' d e f a u l t ' / ) ;
553+ // Resolved before bootstrapping, so nothing reached AWS.
554+ expect ( runs ) . toEqual ( [ ] ) ;
463555 } ) ;
464556
465557 test ( "skips bootstrapping when asked, and still deploys" , async ( ) => {
@@ -469,10 +561,14 @@ describe("FsProjectManager.deploy", () => {
469561 commands . length = 0 ;
470562 const cdkDir = join ( directory , "example" , "agentcore" , "cdk" ) ;
471563
472- const events = await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : true } ) ) ;
564+ const events = await drain (
565+ subject . deploy ( project , { region : REGION , skipBootstrap : true , target : "default" } ) ,
566+ ) ;
473567
474568 expect ( commands ) . toEqual ( [ { command : synthCommand ( directory ) , cwd : cdkDir } ] ) ;
475- expect ( runs . map ( ( { operation } ) => operation ) ) . toEqual ( [ { kind : "deploy" } ] ) ;
569+ expect ( runs . map ( ( { operation } ) => operation ) ) . toEqual ( [
570+ { kind : "deploy" , stackName : stackName ( "default" ) } ,
571+ ] ) ;
476572 expect ( events ) . not . toContainEqual ( { message : "Bootstrapping aws://111122223333/us-east-1" } ) ;
477573 } ) ;
478574
@@ -484,7 +580,7 @@ describe("FsProjectManager.deploy", () => {
484580 commands . length = 0 ;
485581
486582 await expect (
487- drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false } ) ) ,
583+ drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "default" } ) ) ,
488584 ) . rejects . toThrow ( / a w s - t a r g e t s \. j s o n / ) ;
489585 // Nothing ran at all: a deploy with nowhere to go does not even synthesize.
490586 expect ( commands ) . toEqual ( [ ] ) ;
@@ -497,7 +593,7 @@ describe("FsProjectManager.deploy", () => {
497593 const project = await scaffolded ( subject , directory , "{ not a target list" ) ;
498594
499595 await expect (
500- drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false } ) ) ,
596+ drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "default" } ) ) ,
501597 ) . rejects . toThrow ( / i s n o t a v a l i d l i s t o f d e p l o y m e n t t a r g e t s / ) ;
502598 } ) ;
503599
@@ -509,7 +605,7 @@ describe("FsProjectManager.deploy", () => {
509605 ] ) ;
510606
511607 await expect (
512- drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false } ) ) ,
608+ drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "default" } ) ) ,
513609 ) . rejects . toThrow ( / i s n o t a v a l i d l i s t o f d e p l o y m e n t t a r g e t s / ) ;
514610 } ) ;
515611
@@ -522,7 +618,9 @@ describe("FsProjectManager.deploy", () => {
522618 } ) ;
523619 const project = await scaffolded ( subject , directory ) ;
524620
525- const events = await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : true } ) ) ;
621+ const events = await drain (
622+ subject . deploy ( project , { region : REGION , skipBootstrap : true , target : "default" } ) ,
623+ ) ;
526624
527625 expect ( events . map ( ( event ) => event . output ) . filter ( Boolean ) ) . toEqual ( [
528626 "example-stack: creating CloudFormation changeset..." ,
@@ -540,7 +638,9 @@ describe("FsProjectManager.deploy", () => {
540638 } ) ;
541639 const project = await scaffolded ( subject , directory ) ;
542640
543- const events = await drain ( subject . deploy ( project , { region : REGION , skipBootstrap : true } ) ) ;
641+ const events = await drain (
642+ subject . deploy ( project , { region : REGION , skipBootstrap : true , target : "default" } ) ,
643+ ) ;
544644
545645 // The suppressed ones are still in the debug log; only the warning is surfaced.
546646 expect ( events . map ( ( event ) => event . output ) . filter ( Boolean ) ) . toEqual ( [
@@ -558,7 +658,11 @@ describe("FsProjectManager.deploy", () => {
558658 const project = await scaffolded ( subject , directory ) ;
559659
560660 const events : ProjectEvent [ ] = [ ] ;
561- const generator = subject . deploy ( project , { region : REGION , skipBootstrap : true } ) ;
661+ const generator = subject . deploy ( project , {
662+ region : REGION ,
663+ skipBootstrap : true ,
664+ target : "default" ,
665+ } ) ;
562666 await expect (
563667 ( async ( ) => {
564668 for await ( const event of generator ) events . push ( event ) ;
@@ -576,7 +680,7 @@ describe("FsProjectManager.deploy", () => {
576680 // CDK is the only backend today; the cast stands in for a future one.
577681 const foreign = { ...project , managedBy : "Terraform" as Project [ "managedBy" ] } ;
578682 await expect (
579- drain ( subject . deploy ( foreign , { region : REGION , skipBootstrap : false } ) ) ,
683+ drain ( subject . deploy ( foreign , { region : REGION , skipBootstrap : false , target : "default" } ) ) ,
580684 ) . rejects . toThrow ( / u n s u p p o r t e d b a c k e n d : T e r r a f o r m / ) ;
581685 expect ( commands ) . toEqual ( [ ] ) ;
582686 expect ( runs ) . toEqual ( [ ] ) ;
@@ -590,7 +694,7 @@ describe("FsProjectManager.deploy", () => {
590694 commands . length = 0 ;
591695
592696 await expect (
593- drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false } ) ) ,
697+ drain ( subject . deploy ( project , { region : REGION , skipBootstrap : false , target : "default" } ) ) ,
594698 ) . rejects . toThrow ( / n p m i n s t a l l / ) ;
595699 expect ( commands ) . toEqual ( [ ] ) ;
596700 expect ( runs ) . toEqual ( [ ] ) ;
0 commit comments