Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 210 additions & 21 deletions src/core/project/backends/cdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const TARGET = {
} as const;
const STACK_ARN =
"arn:aws:cloudformation:us-east-1:111122223333:stack/AgentCore-example-default/abc";
const ARN = `arn:aws:bedrock-agentcore:${TARGET.region}:${TARGET.account}`;
const json = new FsReadWriteJson({ logger: createSilentLogger() });

/** A template holding only what CDK adds itself, as an empty project synthesizes. */
Expand Down Expand Up @@ -810,8 +811,8 @@ describe("CdkBackend.deploy", () => {
});
});

describe("CdkBackend.resolveDeployedResources", () => {
test("describes the stack once and returns only resources with deployed ID outputs", async () => {
describe("CdkBackend.resolveProjectResources", () => {

@tejaskash tejaskash Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base branch tested -RuntimeId and -Harness-<name>-Id. Now nothing here asks for identifier: "id", so a typo in the suffix logic would make headless invoke report a deployed runtime as not deployed with CI green. One id-mode case would cover it.

test("describes the stack once and reports every declared resource", async () => {
const input = await project();
input.spec = ProjectSpecSchema.parse({
...input.spec,
Expand Down Expand Up @@ -841,59 +842,67 @@ describe("CdkBackend.resolveDeployedResources", () => {
StackStatus: "CREATE_COMPLETE",
Outputs: [
{
ExportName: "AgentCore-example-default-checkout-agent-RuntimeId",
OutputValue: "checkout_agent-AbCdEf1234",
ExportName: "AgentCore-example-default-checkout-agent-RuntimeArn",
OutputValue: `${ARN}:runtime/checkout_agent-AbCdEf1234`,
},
{
ExportName: "AgentCore-example-default-Harness-support-agent-Id",
OutputValue: "support_agent-AbCdEf1234",
ExportName: "AgentCore-example-default-Harness-support-agent-Arn",
OutputValue: `${ARN}:harness/support_agent-AbCdEf1234`,
},
],
},
});

const resources = await subject.backend.resolveDeployedResources(input, { target: TARGET });
const resources = await subject.backend.resolveProjectResources(input, { target: TARGET });

expect(resources).toEqual([
{
resourceType: "runtime",
name: "checkout_agent",
id: "checkout_agent-AbCdEf1234",
deploymentState: "deployed",
id: `${ARN}:runtime/checkout_agent-AbCdEf1234`,
target: TARGET,
},
{
resourceType: "runtime",
name: "inventory",
deploymentState: "local-only",
target: TARGET,
},
{
resourceType: "harness",
name: "support_agent",
id: "support_agent-AbCdEf1234",
deploymentState: "deployed",
id: `${ARN}:harness/support_agent-AbCdEf1234`,
target: TARGET,
},
]);
expect(subject.stackReads).toHaveLength(1);
});

test("fails without reading AWS when the target has no deployed stack ARN", async () => {
test("reports local-only without reading AWS when the target has no deployed stack ARN", async () => {
const input = await project();
const subject = harness({ describedStack: null });

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).rejects.toThrow(/not deployed.*project deploy --target default/s);
const resources = await subject.backend.resolveProjectResources(input, { target: TARGET });

expect(resources.every(({ deploymentState }) => deploymentState === "local-only")).toBe(true);
expect(subject.stackReads).toEqual([]);
expect(subject.accountCredentials).toEqual([]);
});

test("fails actionably when the recorded stack no longer exists", async () => {
test("reports local-only when the recorded stack no longer exists", async () => {
const input = await project();
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({ describedStack: null });

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).rejects.toThrow(/not deployed.*project deploy --target default/s);
const resources = await subject.backend.resolveProjectResources(input, { target: TARGET });

expect(resources.every(({ deploymentState }) => deploymentState === "local-only")).toBe(true);
expect(subject.stackReads[0]?.stackName).toBe(STACK_ARN);
});

test("omits configured resources that have no deployed ID output", async () => {
test("reports configured resources with no deployed ID output as local-only", async () => {
const input = await project();
input.spec = ProjectSpecSchema.parse({
...input.spec,
Expand All @@ -918,8 +927,10 @@ describe("CdkBackend.resolveDeployedResources", () => {
});

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).resolves.toEqual([]);
subject.backend.resolveProjectResources(input, { target: TARGET }),
).resolves.toEqual([
{ resourceType: "runtime", name: "checkout", deploymentState: "local-only", target: TARGET },
]);
});

test("rejects the wrong account before reading CloudFormation", async () => {
Expand All @@ -928,8 +939,186 @@ describe("CdkBackend.resolveDeployedResources", () => {
const subject = harness({ account: "999900001111" });

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
subject.backend.resolveProjectResources(input, { target: TARGET }),
).rejects.toThrow(/expects AWS account 111122223333.*999900001111/s);
expect(subject.stackReads).toEqual([]);
});

test("resolves every deployed resource type: exports, payment OutputKey, credential from state, nested parents, underscores", async () => {
const input = await project();
input.spec = {
...input.spec,
runtimes: [{ name: "web" }],
harnesses: [{ name: "chat" }],
memories: [{ name: "user_mem" }], // underscore must map to -user-mem-
knowledgeBases: [{ name: "kb" }],
credentials: [{ name: "cred" }], // id comes from deployed-state, not outputs
evaluators: [{ name: "ev" }],
onlineEvalConfigs: [{ name: "oe" }],
agentCoreGateways: [{ name: "gw", targets: [{ name: "tgt" }] }],
policyEngines: [{ name: "pe", policies: [{ name: "pol" }] }],
configBundles: [{ name: "cb" }],
payments: [{ name: "pay", connectors: [{ name: "wallet_one" }] }],
} as unknown as typeof input.spec;
await updateTargetState(json, input.rootPath, TARGET.name, {
stackArn: STACK_ARN,
resources: { credentials: { cred: { credentialProviderArn: "arn:aws:cred/cred" } } },
});
const S = "AgentCore-example-default";
const out = (ExportName: string, OutputValue: string) => ({ ExportName, OutputValue });
const subject = harness({
describedStack: {
StackName: S,
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [
out(`${S}-web-RuntimeArn`, `${ARN}:runtime/web-1`),
out(`${S}-Harness-chat-Arn`, `${ARN}:harness/chat-1`),
out(`${S}-Memory-user-mem-Arn`, `${ARN}:memory/mem-1`),
out(`${S}-KnowledgeBase-kb-Arn`, `${ARN}:knowledge-base/kb-1`),
out(`${S}-Evaluator-ev-Arn`, `${ARN}:evaluator/ev-1`),
out(`${S}-OnlineEval-oe-Arn`, `${ARN}:online-eval/oe-1`),
out(`${S}-Gateway-gw-Arn`, `${ARN}:gateway/gw-1`),
out(`${S}-GatewayTarget-tgt-Id`, "tgt-1"),
out(`${S}-PolicyEngine-pe-Arn`, `${ARN}:policy-engine/pe-1`),
out(`${S}-Policy-pe-pol-Arn`, `${ARN}:policy/pol-1`),
out(`${S}-ConfigBundle-cb-Arn`, `${ARN}:config-bundle/cb-1`),
{ OutputKey: "PaymentpayManagerArn", OutputValue: `${ARN}:payment-manager/pay-1` },
{ OutputKey: "PaymentpaywalletoneConnectorId", OutputValue: "conn-1" },
],
},
});

const resources = await subject.backend.resolveProjectResources(input, { target: TARGET });

expect(resources).toEqual([
{
resourceType: "runtime",
name: "web",
deploymentState: "deployed",
id: `${ARN}:runtime/web-1`,
target: TARGET,
},
{
resourceType: "harness",
name: "chat",
deploymentState: "deployed",
id: `${ARN}:harness/chat-1`,
target: TARGET,
},
{
resourceType: "memory",
name: "user_mem",
deploymentState: "deployed",
id: `${ARN}:memory/mem-1`,
target: TARGET,
},
{
resourceType: "knowledge-base",
name: "kb",
deploymentState: "deployed",
id: `${ARN}:knowledge-base/kb-1`,
target: TARGET,
},
{
resourceType: "credential",
name: "cred",
deploymentState: "deployed",
id: "arn:aws:cred/cred",
target: TARGET,
},
{
resourceType: "evaluator",
name: "ev",
deploymentState: "deployed",
id: `${ARN}:evaluator/ev-1`,
target: TARGET,
},
{
resourceType: "online-eval",
name: "oe",
deploymentState: "deployed",
id: `${ARN}:online-eval/oe-1`,
target: TARGET,
},
{
resourceType: "gateway",
name: "gw",
deploymentState: "deployed",
id: `${ARN}:gateway/gw-1`,
target: TARGET,
},
{
resourceType: "gateway-target",
name: "tgt",
parent: "gw",
deploymentState: "deployed",
id: "tgt-1",
target: TARGET,
},
{
resourceType: "policy-engine",
name: "pe",
deploymentState: "deployed",
id: `${ARN}:policy-engine/pe-1`,
target: TARGET,
},
{
resourceType: "policy",
name: "pol",
parent: "pe",
deploymentState: "deployed",
id: `${ARN}:policy/pol-1`,
target: TARGET,
},
{
resourceType: "config-bundle",
name: "cb",
deploymentState: "deployed",
id: `${ARN}:config-bundle/cb-1`,
target: TARGET,
},
{
resourceType: "payment-manager",
name: "pay",
deploymentState: "deployed",
id: `${ARN}:payment-manager/pay-1`,
target: TARGET,
},
{
resourceType: "payment-connector",
name: "wallet_one",
parent: "pay",
deploymentState: "deployed",
id: "conn-1",
target: TARGET,
},
]);
expect(subject.stackReads).toHaveLength(1);
});

test("reports a declared non-runtime resource with no deployed output as local-only", async () => {
const input = await project();
input.spec = {
...input.spec,
runtimes: [],
harnesses: [],
memories: [{ name: "mem" }],
} as unknown as typeof input.spec;
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({
describedStack: {
StackName: "AgentCore-example-default",
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [],
},
});

await expect(
subject.backend.resolveProjectResources(input, { target: TARGET }),
).resolves.toEqual([
{ resourceType: "memory", name: "mem", deploymentState: "local-only", target: TARGET },
]);
});
});
Loading
Loading