Skip to content

Commit 839b32b

Browse files
authored
fix: remove dead preflight patch, proper teardown, optional evo schema fields (#1073)
- Preflight: remove config bundle type patching entirely — the CDK schema does not have a type field, so the patch was dead code - Teardown: delegate to deleteOrphanedABTests and deleteOrphanedHttpGateways instead of raw API calls — reuses stop/poll/delete/role-cleanup logic. Correct ordering: AB tests first (they create rules on gateways), then gateways, then config bundles. - Schema: change configBundles, abTests, httpGateways from .default([]) to .optional() so they don't appear in agentcore.json unless the user opts into those preview features. All access sites updated with ?? [] or ??= guards. Closes #1070
1 parent 0e38e9e commit 839b32b

10 files changed

Lines changed: 95 additions & 83 deletions

integ-tests/add-remove-config-bundle.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('integration: add and remove config-bundle', () => {
4040
expect(json.bundleName).toBe('InlineBundle');
4141

4242
const config = await readProjectConfig(project.projectPath);
43-
const bundle = config.configBundles.find(b => b.name === 'InlineBundle');
43+
const bundle = config.configBundles!.find(b => b.name === 'InlineBundle');
4444
expect(bundle).toBeDefined();
4545
expect(bundle!.type).toBe('ConfigurationBundle');
4646
expect(bundle!.branchName).toBe('mainline');
@@ -68,7 +68,7 @@ describe('integration: add and remove config-bundle', () => {
6868
expect(json.bundleName).toBe('FileBundle');
6969

7070
const config = await readProjectConfig(project.projectPath);
71-
const bundle = config.configBundles.find(b => b.name === 'FileBundle');
71+
const bundle = config.configBundles!.find(b => b.name === 'FileBundle');
7272
expect(bundle).toBeDefined();
7373
expect(Object.keys(bundle!.components)).toHaveLength(2);
7474
});
@@ -102,7 +102,7 @@ describe('integration: add and remove config-bundle', () => {
102102
expect(json.bundleName).toBe('FullOptsBundle');
103103

104104
const config = await readProjectConfig(project.projectPath);
105-
const bundle = config.configBundles.find(b => b.name === 'FullOptsBundle');
105+
const bundle = config.configBundles!.find(b => b.name === 'FullOptsBundle');
106106
expect(bundle).toBeDefined();
107107
expect(bundle!.description).toBe('A bundle with all optional fields');
108108
expect(bundle!.branchName).toBe('feature-branch');
@@ -127,7 +127,7 @@ describe('integration: add and remove config-bundle', () => {
127127
expect(json.bundleName).toBe('PlaceholderBundle');
128128

129129
const config = await readProjectConfig(project.projectPath);
130-
const bundle = config.configBundles.find(b => b.name === 'PlaceholderBundle');
130+
const bundle = config.configBundles!.find(b => b.name === 'PlaceholderBundle');
131131
expect(bundle).toBeDefined();
132132
const keys = Object.keys(bundle!.components);
133133
expect(keys).toContain('{{runtime:AgentA}}');
@@ -236,7 +236,7 @@ describe('integration: add and remove config-bundle', () => {
236236
expect(json.success).toBe(true);
237237

238238
const config = await readProjectConfig(project.projectPath);
239-
const bundle = config.configBundles.find(b => b.name === 'InlineBundle');
239+
const bundle = config.configBundles!.find(b => b.name === 'InlineBundle');
240240
expect(bundle).toBeUndefined();
241241
});
242242

@@ -251,14 +251,14 @@ describe('integration: add and remove config-bundle', () => {
251251

252252
it('removes all remaining config bundles one by one', async () => {
253253
const configBefore = await readProjectConfig(project.projectPath);
254-
const remaining = configBefore.configBundles.map(b => b.name);
254+
const remaining = configBefore.configBundles!.map(b => b.name);
255255

256256
for (const name of remaining) {
257257
await runSuccess(['remove', 'config-bundle', '--name', name, '--json'], project.projectPath);
258258
}
259259

260260
const configAfter = await readProjectConfig(project.projectPath);
261-
expect(configAfter.configBundles).toHaveLength(0);
261+
expect(configAfter.configBundles!).toHaveLength(0);
262262
});
263263
});
264264

@@ -282,21 +282,21 @@ describe('integration: add and remove config-bundle', () => {
282282
}
283283

284284
const config = await readProjectConfig(project.projectPath);
285-
expect(config.configBundles).toHaveLength(bundleNames.length);
285+
expect(config.configBundles!).toHaveLength(bundleNames.length);
286286

287287
for (const name of bundleNames) {
288-
expect(config.configBundles.find(b => b.name === name)).toBeDefined();
288+
expect(config.configBundles!.find(b => b.name === name)).toBeDefined();
289289
}
290290
});
291291

292292
it('removing one bundle does not affect others', async () => {
293293
await runSuccess(['remove', 'config-bundle', '--name', 'BundleBeta', '--json'], project.projectPath);
294294

295295
const config = await readProjectConfig(project.projectPath);
296-
expect(config.configBundles).toHaveLength(2);
297-
expect(config.configBundles.find(b => b.name === 'BundleAlpha')).toBeDefined();
298-
expect(config.configBundles.find(b => b.name === 'BundleGamma')).toBeDefined();
299-
expect(config.configBundles.find(b => b.name === 'BundleBeta')).toBeUndefined();
296+
expect(config.configBundles!).toHaveLength(2);
297+
expect(config.configBundles!.find(b => b.name === 'BundleAlpha')).toBeDefined();
298+
expect(config.configBundles!.find(b => b.name === 'BundleGamma')).toBeDefined();
299+
expect(config.configBundles!.find(b => b.name === 'BundleBeta')).toBeUndefined();
300300
});
301301

302302
afterAll(async () => {

src/cli/operations/ab-test/promote.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ export async function promoteABTestConfig(abTestId: string, testNameFallback?: s
5555
`[promote] Could not resolve AB test ID "${abTestId}" from deployed state; falling back to name "${testNameFallback}".`
5656
);
5757
const lowerName = testNameFallback.toLowerCase();
58-
const match = project.abTests.find(
58+
const match = (project.abTests ?? []).find(
5959
t => t.name.toLowerCase() === lowerName || `${project.name}_${t.name}`.toLowerCase() === lowerName
6060
);
6161
specName = match?.name;
6262
}
6363

64-
const abTest = specName ? project.abTests.find(t => t.name === specName) : undefined;
64+
const abTest = specName ? (project.abTests ?? []).find(t => t.name === specName) : undefined;
6565

6666
if (!abTest) {
6767
return { promoted: false, promotionDetail: `AB test with ID "${abTestId}" not found in project config.` };
@@ -78,7 +78,7 @@ export async function promoteABTestConfig(abTestId: string, testNameFallback?: s
7878
const gwMatch = /^\{\{gateway:(.+)\}\}$/.exec(abTest.gatewayRef);
7979
const gwName = gwMatch?.[1];
8080
if (gwName) {
81-
const gw = project.httpGateways.find(g => g.name === gwName);
81+
const gw = (project.httpGateways ?? []).find(g => g.name === gwName);
8282
if (gw?.targets) {
8383
const controlTarget = gw.targets.find(t => t.name === controlTargetName);
8484
const treatmentTarget = gw.targets.find(t => t.name === treatmentTargetName);

src/cli/operations/agent/config-bundle-defaults.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ export async function createConfigBundleForAgent(agentName: string, configBaseDi
55
const project = await configIO.readProjectSpec();
66

77
const bundleName = `${agentName}Config`;
8-
if (project.configBundles.some(b => b.name === bundleName)) return;
8+
if ((project.configBundles ?? []).some(b => b.name === bundleName)) return;
99

10+
project.configBundles ??= [];
1011
project.configBundles.push({
1112
type: 'ConfigurationBundle',
1213
name: bundleName,

src/cli/operations/deploy/__tests__/post-deploy-config-bundles.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ describe('resolveConfigBundleComponentKeys', () => {
537537
});
538538

539539
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
540-
const keys = Object.keys(result.configBundles[0]!.components);
540+
const keys = Object.keys(result.configBundles![0]!.components);
541541
expect(keys).toEqual(['arn:aws:bedrock-agentcore:us-east-1:123:runtime/rt-1']);
542542
});
543543

@@ -550,7 +550,7 @@ describe('resolveConfigBundleComponentKeys', () => {
550550
});
551551

552552
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
553-
const keys = Object.keys(result.configBundles[0]!.components);
553+
const keys = Object.keys(result.configBundles![0]!.components);
554554
expect(keys).toEqual(['arn:aws:bedrock-agentcore:us-east-1:123:gateway/gw-1']);
555555
});
556556

@@ -563,7 +563,7 @@ describe('resolveConfigBundleComponentKeys', () => {
563563
});
564564

565565
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
566-
const keys = Object.keys(result.configBundles[0]!.components);
566+
const keys = Object.keys(result.configBundles![0]!.components);
567567
expect(keys).toEqual(['arn:mcp:gw:resolved']);
568568
});
569569

@@ -574,7 +574,7 @@ describe('resolveConfigBundleComponentKeys', () => {
574574
const deployedState = makeDeployedState('target1', { runtimes: {} });
575575

576576
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
577-
const keys = Object.keys(result.configBundles[0]!.components);
577+
const keys = Object.keys(result.configBundles![0]!.components);
578578
expect(keys).toEqual(['arn:existing:key']);
579579
});
580580

@@ -585,7 +585,7 @@ describe('resolveConfigBundleComponentKeys', () => {
585585
const deployedState = makeDeployedState('target1', { runtimes: {} });
586586

587587
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
588-
const keys = Object.keys(result.configBundles[0]!.components);
588+
const keys = Object.keys(result.configBundles![0]!.components);
589589
expect(keys).toEqual(['some-plain-key']);
590590
});
591591

@@ -629,9 +629,9 @@ describe('resolveConfigBundleComponentKeys', () => {
629629

630630
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
631631
// Original should still have the placeholder
632-
expect(Object.keys(spec.configBundles[0]!.components)).toEqual(['{{runtime:my-rt}}']);
632+
expect(Object.keys(spec.configBundles![0]!.components)).toEqual(['{{runtime:my-rt}}']);
633633
// Result should have the resolved key
634-
expect(Object.keys(result.configBundles[0]!.components)).toEqual(['arn:resolved']);
634+
expect(Object.keys(result.configBundles![0]!.components)).toEqual(['arn:resolved']);
635635
});
636636

637637
it('prefers HTTP gateway over MCP gateway when both exist with same name', () => {
@@ -644,7 +644,7 @@ describe('resolveConfigBundleComponentKeys', () => {
644644
});
645645

646646
const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1');
647-
const keys = Object.keys(result.configBundles[0]!.components);
647+
const keys = Object.keys(result.configBundles![0]!.components);
648648
// HTTP gateway should take precedence (checked first in code)
649649
expect(keys).toEqual(['arn:http:gw']);
650650
});

src/cli/operations/deploy/post-deploy-ab-tests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export async function setupABTests(options: SetupABTestsOptions): Promise<SetupA
134134
const abTests: Record<string, ABTestDeployedState> = {};
135135

136136
// Create or skip tests from the spec
137-
for (const testSpec of projectSpec.abTests) {
137+
for (const testSpec of projectSpec.abTests ?? []) {
138138
let resolvedRoleArn: string | undefined;
139139
let roleCreatedByCli = false;
140140
try {
@@ -321,7 +321,7 @@ export async function deleteOrphanedABTests(options: {
321321
const { region, projectSpec, existingABTests } = options;
322322
if (!existingABTests) return { results: [], hasErrors: false };
323323

324-
const specTestNames = new Set(projectSpec.abTests.map(t => t.name));
324+
const specTestNames = new Set((projectSpec.abTests ?? []).map(t => t.name));
325325
const results: ABTestSetupResult[] = [];
326326

327327
for (const [testName, testState] of Object.entries(existingABTests)) {

src/cli/operations/deploy/post-deploy-config-bundles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ export async function setupConfigBundles(options: SetupConfigBundlesOptions): Pr
5353
const results: ConfigBundleSetupResult[] = [];
5454
const configBundles: Record<string, ConfigBundleDeployedState> = {};
5555

56-
const specBundleNames = new Set(projectSpec.configBundles.map(b => b.name));
56+
const specBundleNames = new Set((projectSpec.configBundles ?? []).map(b => b.name));
5757
const projectName = projectSpec.name;
5858

5959
// Create or update bundles from the spec
60-
for (const bundleSpec of projectSpec.configBundles) {
60+
for (const bundleSpec of projectSpec.configBundles ?? []) {
6161
// Prepend project name to the API-side bundle name (no separator for config bundles)
6262
const apiBundleName = `${projectName}${bundleSpec.name}`;
6363

src/cli/operations/deploy/post-deploy-http-gateways.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ export async function deleteOrphanedHttpGateways(options: {
420420
const { region, projectSpec, existingHttpGateways } = options;
421421
if (!existingHttpGateways) return { results: [], hasErrors: false };
422422

423-
const specGatewayNames = new Set(projectSpec.httpGateways.map(g => g.name));
423+
const specGatewayNames = new Set((projectSpec.httpGateways ?? []).map(g => g.name));
424424
const results: HttpGatewaySetupResult[] = [];
425425

426426
for (const [gwName, gwState] of Object.entries(existingHttpGateways)) {

0 commit comments

Comments
 (0)