@@ -10,9 +10,10 @@ import { createSilentLogger } from "../../../testing";
1010import { CdkBackend } from "./cdk" ;
1111import type {
1212 CredentialProviderCalls ,
13+ CredentialProvisionInput ,
1314 CredentialProvisioner ,
14- DeployedCredentials ,
15- PaymentCredentialRemover ,
15+ CredentialRemovalInput ,
16+ CredentialRemover ,
1617} from "./cdk/credentials" ;
1718import { DEPLOYED_STATE_RELATIVE_PATH , updateTargetState } from "./cdk/deployedState" ;
1819import type { DeployBackendInput } from "./types" ;
@@ -157,7 +158,7 @@ type HarnessOptions = {
157158 failOperation ?: CdkOperation [ "kind" ] ;
158159 bootstrapError ?: Error ;
159160 provisionCredentials ?: CredentialProvisioner ;
160- removePaymentCredentials ?: PaymentCredentialRemover ;
161+ removeCredentials ?: CredentialRemover ;
161162 /** Stack returned by CloudFormation. Defaults to a present stack; null means absent. */
162163 describedStack ?: Stack | null ;
163164 /** Chunks the fake synth process streams through onOutput. */
@@ -237,8 +238,8 @@ function harness(options: HarnessOptions = {}) {
237238 } ;
238239 } ,
239240 ...( options . provisionCredentials && { provisionCredentials : options . provisionCredentials } ) ,
240- ...( options . removePaymentCredentials && {
241- removePaymentCredentials : options . removePaymentCredentials ,
241+ ...( options . removeCredentials && {
242+ removeCredentials : options . removeCredentials ,
242243 } ) ,
243244 describeStack : async ( region , provider , stackName ) => {
244245 stackReads . push ( { stackName, region, credentials : provider } ) ;
@@ -453,10 +454,12 @@ describe("CdkBackend.deploy", () => {
453454 } ) ;
454455 } ) ;
455456
456- test ( "provisions credentials before synth and records them under the target " , async ( ) => {
457+ test ( "provisions credentials for the target before synth and records them under it " , async ( ) => {
457458 const input = await project ( ) ;
458459 await writeAssembly ( input , [ TARGET . name ] ) ;
459- const provisionCredentials : CredentialProvisioner = async function * ( ) {
460+ const handed : CredentialProvisionInput [ ] = [ ] ;
461+ const provisionCredentials : CredentialProvisioner = async function * ( _project , provisionInput ) {
462+ handed . push ( provisionInput ) ;
460463 yield { type : "step" , message : "Preparing credential provider 'openai-key'" } ;
461464 return { "openai-key" : { credentialProviderArn : "arn:apikey:openai-key" } } ;
462465 } ;
@@ -468,6 +471,10 @@ describe("CdkBackend.deploy", () => {
468471
469472 const deployed = await collectDeploy ( subject . backend . deploy ( input , deployInput ( ) ) ) ;
470473
474+ // The target's name scopes the provider names, so provisioning needs it.
475+ expect ( handed ) . toEqual ( [
476+ { credentials : subject . credentials , region : TARGET . region , targetName : TARGET . name } ,
477+ ] ) ;
471478 // The credential step runs (and its ARNs are recorded) before synthesis, so
472479 // the assembly is synthesized against a state file that already describes them.
473480 const messages = stepMessages ( deployed . events ) ;
@@ -640,15 +647,15 @@ describe("CdkBackend.deploy", () => {
640647 } ) ;
641648 } ) ;
642649
643- test ( "removes the project's payment credential providers after its stack" , async ( ) => {
650+ test ( "removes the target's credential providers after its stack" , async ( ) => {
644651 const input = await project ( ) ;
645652 await writeAssembly ( input , [ TARGET . name ] , { resources : METADATA_ONLY } ) ;
646653 const removals : string [ ] = [ ] ;
647- const removePaymentCredentials : PaymentCredentialRemover = async function * ( project ) {
654+ const removeCredentials : CredentialRemover = async function * ( project ) {
648655 removals . push ( project . name ) ;
649656 yield { type : "step" , message : "Removing credential provider 'wallet'" } ;
650657 } ;
651- const subject = harness ( { removePaymentCredentials } ) ;
658+ const subject = harness ( { removeCredentials } ) ;
652659
653660 const deployed = await collectDeploy (
654661 subject . backend . deploy ( input , deployInput ( { confirmTeardown : async ( ) => true } ) ) ,
@@ -662,10 +669,11 @@ describe("CdkBackend.deploy", () => {
662669 ) ;
663670 } ) ;
664671
665- test ( "hands teardown the credentials recorded before the deploy overwrote them" , async ( ) => {
672+ test ( "hands teardown the target and the providers recorded before the deploy overwrote them" , async ( ) => {
666673 // The `project remove all` shape: the spec declares nothing, so provisioning
667674 // returns nothing and rewrites the credentials map to empty before teardown runs.
668- // The recorded providers are the only remaining record of what to delete.
675+ // The recorded providers are the only remaining record of what to delete, and
676+ // the target name is what scopes their provider names.
669677 const input = await project ( ) ;
670678 await writeAssembly ( input , [ TARGET . name ] , { resources : METADATA_ONLY } ) ;
671679 const statePath = join ( input . rootPath , DEPLOYED_STATE_RELATIVE_PATH ) ;
@@ -686,21 +694,88 @@ describe("CdkBackend.deploy", () => {
686694 } ) ,
687695 ) ;
688696
689- const handed : DeployedCredentials [ ] = [ ] ;
690- const removePaymentCredentials : PaymentCredentialRemover = async function * (
691- _project ,
692- { recorded } ,
693- ) {
694- handed . push ( recorded ) ;
697+ const handed : CredentialRemovalInput [ ] = [ ] ;
698+ const removeCredentials : CredentialRemover = async function * ( _project , removalInput ) {
699+ handed . push ( removalInput ) ;
695700 yield { type : "step" , message : "Removing credential provider 'wallet'" } ;
696701 } ;
697- const subject = harness ( { removePaymentCredentials } ) ;
702+ const subject = harness ( { removeCredentials } ) ;
698703
699704 await collectDeploy (
700705 subject . backend . deploy ( input , deployInput ( { confirmTeardown : async ( ) => true } ) ) ,
701706 ) ;
702707
703- expect ( handed ) . toEqual ( [ { wallet : recordedWallet } ] ) ;
708+ expect ( handed ) . toEqual ( [
709+ {
710+ credentials : subject . credentials ,
711+ region : TARGET . region ,
712+ targetName : TARGET . name ,
713+ providers : [ { name : "wallet" , authorizerType : "PaymentCredentialProvider" } ] ,
714+ } ,
715+ ] ) ;
716+ } ) ;
717+
718+ test ( "removes a recorded provider whose credential left the spec, after the stack update" , async ( ) => {
719+ const input = await project ( ) ;
720+ input . spec = ProjectSpecSchema . parse ( {
721+ name : "example" ,
722+ version : 1 ,
723+ credentials : [ { authorizerType : "ApiKeyCredentialProvider" , name : "openai-key" } ] ,
724+ } ) ;
725+ await writeAssembly ( input , [ TARGET . name ] ) ;
726+ const statePath = join ( input . rootPath , DEPLOYED_STATE_RELATIVE_PATH ) ;
727+ await mkdir ( dirname ( statePath ) , { recursive : true } ) ;
728+ await writeFile (
729+ statePath ,
730+ JSON . stringify ( {
731+ targets : {
732+ default : {
733+ stackArn : "arn:stack:default" ,
734+ resources : {
735+ credentials : {
736+ "openai-key" : {
737+ credentialProviderArn : "arn:apikey:openai-key" ,
738+ authorizerType : "ApiKeyCredentialProvider" ,
739+ } ,
740+ wallet : {
741+ credentialProviderArn : "arn:payment:wallet" ,
742+ authorizerType : "PaymentCredentialProvider" ,
743+ } ,
744+ } ,
745+ } ,
746+ } ,
747+ } ,
748+ } ) ,
749+ ) ;
750+ const provisionCredentials : CredentialProvisioner = async function * ( ) {
751+ yield { type : "step" , message : "Preparing credential provider 'example_default_openai-key'" } ;
752+ return { "openai-key" : { credentialProviderArn : "arn:apikey:openai-key" } } ;
753+ } ;
754+ const handed : CredentialRemovalInput [ ] = [ ] ;
755+ const removeCredentials : CredentialRemover = async function * ( _project , removalInput ) {
756+ handed . push ( removalInput ) ;
757+ yield { type : "step" , message : "Removing credential provider 'example_default_wallet'" } ;
758+ } ;
759+ const subject = harness ( { provisionCredentials, removeCredentials } ) ;
760+
761+ const deployed = await collectDeploy ( subject . backend . deploy ( input , deployInput ( ) ) ) ;
762+
763+ // Only the dropped credential's provider goes: the declared one was just provisioned.
764+ expect ( handed ) . toEqual ( [
765+ {
766+ credentials : subject . credentials ,
767+ region : TARGET . region ,
768+ targetName : TARGET . name ,
769+ providers : [ { name : "wallet" , authorizerType : "PaymentCredentialProvider" } ] ,
770+ } ,
771+ ] ) ;
772+ // After the stack update, since a resource in the stack may have used the
773+ // provider until this deploy removed the reference.
774+ const messages = stepMessages ( deployed . events ) ;
775+ expect ( messages . indexOf ( "Deploying AgentCore-example-default-0" ) ) . toBeLessThan (
776+ messages . indexOf ( "Removing credential provider 'example_default_wallet'" ) ,
777+ ) ;
778+ expect ( deployed . result ) . toEqual ( { outputs : { } } ) ;
704779 } ) ;
705780
706781 test ( "says to add a resource when there is no stack to remove either" , async ( ) => {
0 commit comments