diff --git a/integ-tests/add-remove-config-bundle.test.ts b/integ-tests/add-remove-config-bundle.test.ts index a7d68ccf4..bd53e7f31 100644 --- a/integ-tests/add-remove-config-bundle.test.ts +++ b/integ-tests/add-remove-config-bundle.test.ts @@ -40,7 +40,7 @@ describe('integration: add and remove config-bundle', () => { expect(json.bundleName).toBe('InlineBundle'); const config = await readProjectConfig(project.projectPath); - const bundle = config.configBundles.find(b => b.name === 'InlineBundle'); + const bundle = config.configBundles!.find(b => b.name === 'InlineBundle'); expect(bundle).toBeDefined(); expect(bundle!.type).toBe('ConfigurationBundle'); expect(bundle!.branchName).toBe('mainline'); @@ -68,7 +68,7 @@ describe('integration: add and remove config-bundle', () => { expect(json.bundleName).toBe('FileBundle'); const config = await readProjectConfig(project.projectPath); - const bundle = config.configBundles.find(b => b.name === 'FileBundle'); + const bundle = config.configBundles!.find(b => b.name === 'FileBundle'); expect(bundle).toBeDefined(); expect(Object.keys(bundle!.components)).toHaveLength(2); }); @@ -102,7 +102,7 @@ describe('integration: add and remove config-bundle', () => { expect(json.bundleName).toBe('FullOptsBundle'); const config = await readProjectConfig(project.projectPath); - const bundle = config.configBundles.find(b => b.name === 'FullOptsBundle'); + const bundle = config.configBundles!.find(b => b.name === 'FullOptsBundle'); expect(bundle).toBeDefined(); expect(bundle!.description).toBe('A bundle with all optional fields'); expect(bundle!.branchName).toBe('feature-branch'); @@ -127,7 +127,7 @@ describe('integration: add and remove config-bundle', () => { expect(json.bundleName).toBe('PlaceholderBundle'); const config = await readProjectConfig(project.projectPath); - const bundle = config.configBundles.find(b => b.name === 'PlaceholderBundle'); + const bundle = config.configBundles!.find(b => b.name === 'PlaceholderBundle'); expect(bundle).toBeDefined(); const keys = Object.keys(bundle!.components); expect(keys).toContain('{{runtime:AgentA}}'); @@ -236,7 +236,7 @@ describe('integration: add and remove config-bundle', () => { expect(json.success).toBe(true); const config = await readProjectConfig(project.projectPath); - const bundle = config.configBundles.find(b => b.name === 'InlineBundle'); + const bundle = config.configBundles!.find(b => b.name === 'InlineBundle'); expect(bundle).toBeUndefined(); }); @@ -251,14 +251,14 @@ describe('integration: add and remove config-bundle', () => { it('removes all remaining config bundles one by one', async () => { const configBefore = await readProjectConfig(project.projectPath); - const remaining = configBefore.configBundles.map(b => b.name); + const remaining = configBefore.configBundles!.map(b => b.name); for (const name of remaining) { await runSuccess(['remove', 'config-bundle', '--name', name, '--json'], project.projectPath); } const configAfter = await readProjectConfig(project.projectPath); - expect(configAfter.configBundles).toHaveLength(0); + expect(configAfter.configBundles!).toHaveLength(0); }); }); @@ -282,10 +282,10 @@ describe('integration: add and remove config-bundle', () => { } const config = await readProjectConfig(project.projectPath); - expect(config.configBundles).toHaveLength(bundleNames.length); + expect(config.configBundles!).toHaveLength(bundleNames.length); for (const name of bundleNames) { - expect(config.configBundles.find(b => b.name === name)).toBeDefined(); + expect(config.configBundles!.find(b => b.name === name)).toBeDefined(); } }); @@ -293,10 +293,10 @@ describe('integration: add and remove config-bundle', () => { await runSuccess(['remove', 'config-bundle', '--name', 'BundleBeta', '--json'], project.projectPath); const config = await readProjectConfig(project.projectPath); - expect(config.configBundles).toHaveLength(2); - expect(config.configBundles.find(b => b.name === 'BundleAlpha')).toBeDefined(); - expect(config.configBundles.find(b => b.name === 'BundleGamma')).toBeDefined(); - expect(config.configBundles.find(b => b.name === 'BundleBeta')).toBeUndefined(); + expect(config.configBundles!).toHaveLength(2); + expect(config.configBundles!.find(b => b.name === 'BundleAlpha')).toBeDefined(); + expect(config.configBundles!.find(b => b.name === 'BundleGamma')).toBeDefined(); + expect(config.configBundles!.find(b => b.name === 'BundleBeta')).toBeUndefined(); }); afterAll(async () => { diff --git a/src/cli/operations/ab-test/promote.ts b/src/cli/operations/ab-test/promote.ts index 812b780c9..5f98e52f6 100644 --- a/src/cli/operations/ab-test/promote.ts +++ b/src/cli/operations/ab-test/promote.ts @@ -55,13 +55,13 @@ export async function promoteABTestConfig(abTestId: string, testNameFallback?: s `[promote] Could not resolve AB test ID "${abTestId}" from deployed state; falling back to name "${testNameFallback}".` ); const lowerName = testNameFallback.toLowerCase(); - const match = project.abTests.find( + const match = (project.abTests ?? []).find( t => t.name.toLowerCase() === lowerName || `${project.name}_${t.name}`.toLowerCase() === lowerName ); specName = match?.name; } - const abTest = specName ? project.abTests.find(t => t.name === specName) : undefined; + const abTest = specName ? (project.abTests ?? []).find(t => t.name === specName) : undefined; if (!abTest) { 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 const gwMatch = /^\{\{gateway:(.+)\}\}$/.exec(abTest.gatewayRef); const gwName = gwMatch?.[1]; if (gwName) { - const gw = project.httpGateways.find(g => g.name === gwName); + const gw = (project.httpGateways ?? []).find(g => g.name === gwName); if (gw?.targets) { const controlTarget = gw.targets.find(t => t.name === controlTargetName); const treatmentTarget = gw.targets.find(t => t.name === treatmentTargetName); diff --git a/src/cli/operations/agent/config-bundle-defaults.ts b/src/cli/operations/agent/config-bundle-defaults.ts index f25b450ac..25db93003 100644 --- a/src/cli/operations/agent/config-bundle-defaults.ts +++ b/src/cli/operations/agent/config-bundle-defaults.ts @@ -5,8 +5,9 @@ export async function createConfigBundleForAgent(agentName: string, configBaseDi const project = await configIO.readProjectSpec(); const bundleName = `${agentName}Config`; - if (project.configBundles.some(b => b.name === bundleName)) return; + if ((project.configBundles ?? []).some(b => b.name === bundleName)) return; + project.configBundles ??= []; project.configBundles.push({ type: 'ConfigurationBundle', name: bundleName, diff --git a/src/cli/operations/deploy/__tests__/post-deploy-config-bundles.test.ts b/src/cli/operations/deploy/__tests__/post-deploy-config-bundles.test.ts index ecfc285cd..0981efcff 100644 --- a/src/cli/operations/deploy/__tests__/post-deploy-config-bundles.test.ts +++ b/src/cli/operations/deploy/__tests__/post-deploy-config-bundles.test.ts @@ -537,7 +537,7 @@ describe('resolveConfigBundleComponentKeys', () => { }); const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); - const keys = Object.keys(result.configBundles[0]!.components); + const keys = Object.keys(result.configBundles![0]!.components); expect(keys).toEqual(['arn:aws:bedrock-agentcore:us-east-1:123:runtime/rt-1']); }); @@ -550,7 +550,7 @@ describe('resolveConfigBundleComponentKeys', () => { }); const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); - const keys = Object.keys(result.configBundles[0]!.components); + const keys = Object.keys(result.configBundles![0]!.components); expect(keys).toEqual(['arn:aws:bedrock-agentcore:us-east-1:123:gateway/gw-1']); }); @@ -563,7 +563,7 @@ describe('resolveConfigBundleComponentKeys', () => { }); const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); - const keys = Object.keys(result.configBundles[0]!.components); + const keys = Object.keys(result.configBundles![0]!.components); expect(keys).toEqual(['arn:mcp:gw:resolved']); }); @@ -574,7 +574,7 @@ describe('resolveConfigBundleComponentKeys', () => { const deployedState = makeDeployedState('target1', { runtimes: {} }); const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); - const keys = Object.keys(result.configBundles[0]!.components); + const keys = Object.keys(result.configBundles![0]!.components); expect(keys).toEqual(['arn:existing:key']); }); @@ -585,7 +585,7 @@ describe('resolveConfigBundleComponentKeys', () => { const deployedState = makeDeployedState('target1', { runtimes: {} }); const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); - const keys = Object.keys(result.configBundles[0]!.components); + const keys = Object.keys(result.configBundles![0]!.components); expect(keys).toEqual(['some-plain-key']); }); @@ -629,9 +629,9 @@ describe('resolveConfigBundleComponentKeys', () => { const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); // Original should still have the placeholder - expect(Object.keys(spec.configBundles[0]!.components)).toEqual(['{{runtime:my-rt}}']); + expect(Object.keys(spec.configBundles![0]!.components)).toEqual(['{{runtime:my-rt}}']); // Result should have the resolved key - expect(Object.keys(result.configBundles[0]!.components)).toEqual(['arn:resolved']); + expect(Object.keys(result.configBundles![0]!.components)).toEqual(['arn:resolved']); }); it('prefers HTTP gateway over MCP gateway when both exist with same name', () => { @@ -644,7 +644,7 @@ describe('resolveConfigBundleComponentKeys', () => { }); const result = resolveConfigBundleComponentKeys(spec, deployedState, 'target1'); - const keys = Object.keys(result.configBundles[0]!.components); + const keys = Object.keys(result.configBundles![0]!.components); // HTTP gateway should take precedence (checked first in code) expect(keys).toEqual(['arn:http:gw']); }); diff --git a/src/cli/operations/deploy/post-deploy-ab-tests.ts b/src/cli/operations/deploy/post-deploy-ab-tests.ts index 63ec1cd44..4678d16f8 100644 --- a/src/cli/operations/deploy/post-deploy-ab-tests.ts +++ b/src/cli/operations/deploy/post-deploy-ab-tests.ts @@ -134,7 +134,7 @@ export async function setupABTests(options: SetupABTestsOptions): Promise = {}; // Create or skip tests from the spec - for (const testSpec of projectSpec.abTests) { + for (const testSpec of projectSpec.abTests ?? []) { let resolvedRoleArn: string | undefined; let roleCreatedByCli = false; try { @@ -321,7 +321,7 @@ export async function deleteOrphanedABTests(options: { const { region, projectSpec, existingABTests } = options; if (!existingABTests) return { results: [], hasErrors: false }; - const specTestNames = new Set(projectSpec.abTests.map(t => t.name)); + const specTestNames = new Set((projectSpec.abTests ?? []).map(t => t.name)); const results: ABTestSetupResult[] = []; for (const [testName, testState] of Object.entries(existingABTests)) { diff --git a/src/cli/operations/deploy/post-deploy-config-bundles.ts b/src/cli/operations/deploy/post-deploy-config-bundles.ts index 72c8752ba..5318c54b1 100644 --- a/src/cli/operations/deploy/post-deploy-config-bundles.ts +++ b/src/cli/operations/deploy/post-deploy-config-bundles.ts @@ -53,11 +53,11 @@ export async function setupConfigBundles(options: SetupConfigBundlesOptions): Pr const results: ConfigBundleSetupResult[] = []; const configBundles: Record = {}; - const specBundleNames = new Set(projectSpec.configBundles.map(b => b.name)); + const specBundleNames = new Set((projectSpec.configBundles ?? []).map(b => b.name)); const projectName = projectSpec.name; // Create or update bundles from the spec - for (const bundleSpec of projectSpec.configBundles) { + for (const bundleSpec of projectSpec.configBundles ?? []) { // Prepend project name to the API-side bundle name (no separator for config bundles) const apiBundleName = `${projectName}${bundleSpec.name}`; diff --git a/src/cli/operations/deploy/post-deploy-http-gateways.ts b/src/cli/operations/deploy/post-deploy-http-gateways.ts index edaf852ec..db18d28a2 100644 --- a/src/cli/operations/deploy/post-deploy-http-gateways.ts +++ b/src/cli/operations/deploy/post-deploy-http-gateways.ts @@ -420,7 +420,7 @@ export async function deleteOrphanedHttpGateways(options: { const { region, projectSpec, existingHttpGateways } = options; if (!existingHttpGateways) return { results: [], hasErrors: false }; - const specGatewayNames = new Set(projectSpec.httpGateways.map(g => g.name)); + const specGatewayNames = new Set((projectSpec.httpGateways ?? []).map(g => g.name)); const results: HttpGatewaySetupResult[] = []; for (const [gwName, gwState] of Object.entries(existingHttpGateways)) { diff --git a/src/cli/primitives/ABTestPrimitive.ts b/src/cli/primitives/ABTestPrimitive.ts index e62188abb..0a809686a 100644 --- a/src/cli/primitives/ABTestPrimitive.ts +++ b/src/cli/primitives/ABTestPrimitive.ts @@ -73,13 +73,13 @@ export class ABTestPrimitive extends BasePrimitive t.name === testName); + const index = (project.abTests ?? []).findIndex(t => t.name === testName); if (index === -1) { return { success: false, error: `AB test "${testName}" not found.` }; } - const removedTest = project.abTests[index]!; - project.abTests.splice(index, 1); + const removedTest = project.abTests![index]!; + project.abTests!.splice(index, 1); // Cascade: remove auto-created online eval configs for target-based tests // Only remove eval configs that were auto-created (matching the {testName}_eval_ prefix pattern) @@ -102,19 +102,19 @@ export class ABTestPrimitive extends BasePrimitive v.variantConfiguration.target?.targetName) .filter((n): n is string => !!n); - const gw = project.httpGateways.find(g => g.name === gwName); + const gw = (project.httpGateways ?? []).find(g => g.name === gwName); if (gw?.targets) { gw.targets = gw.targets.filter(t => !targetNames.includes(t.name)); } } // Remove gateway if no other AB tests reference it - const stillReferenced = project.abTests.some(t => { + const stillReferenced = (project.abTests ?? []).some(t => { const m = /^\{\{gateway:(.+)\}\}$/.exec(t.gatewayRef); return m?.[1] === gwName; }); if (!stillReferenced) { - project.httpGateways = project.httpGateways.filter(gw => gw.name !== gwName); + project.httpGateways = (project.httpGateways ?? []).filter(gw => gw.name !== gwName); } } } @@ -130,7 +130,7 @@ export class ABTestPrimitive extends BasePrimitive { const project = await this.readProjectSpec(); - const abTest = project.abTests.find(t => t.name === testName); + const abTest = (project.abTests ?? []).find(t => t.name === testName); if (!abTest) { throw new Error(`AB test "${testName}" not found.`); } @@ -138,27 +138,27 @@ export class ABTestPrimitive extends BasePrimitive t.name === testName); + const testIndex = (project.abTests ?? []).findIndex(t => t.name === testName); const afterSpec = { ...project, - abTests: project.abTests.filter(t => t.name !== testName), - httpGateways: [...project.httpGateways], + abTests: (project.abTests ?? []).filter(t => t.name !== testName), + httpGateways: [...(project.httpGateways ?? [])], }; // Check if the gateway would be orphaned - const test = project.abTests[testIndex]; + const test = (project.abTests ?? [])[testIndex]; if (test?.gatewayRef) { const gwMatch = /^\{\{gateway:(.+)\}\}$/.exec(test.gatewayRef); if (gwMatch) { const gwName = gwMatch[1]; - const otherTests = project.abTests.filter((_, i) => i !== testIndex); + const otherTests = (project.abTests ?? []).filter((_, i) => i !== testIndex); const stillReferenced = otherTests.some(t => { const m = /^\{\{gateway:(.+)\}\}$/.exec(t.gatewayRef); return m && m[1] === gwName; }); if (!stillReferenced) { summary.push(`Also removing HTTP gateway: ${gwName} (no other AB tests reference it)`); - afterSpec.httpGateways = project.httpGateways.filter(gw => gw.name !== gwName); + afterSpec.httpGateways = (project.httpGateways ?? []).filter(gw => gw.name !== gwName); } } } @@ -175,7 +175,7 @@ export class ABTestPrimitive extends BasePrimitive { try { const project = await this.readProjectSpec(); - return project.abTests.map(t => ({ name: t.name })); + return (project.abTests ?? []).map(t => ({ name: t.name })); } catch { return []; } @@ -184,7 +184,7 @@ export class ABTestPrimitive extends BasePrimitive { try { const project = await this.readProjectSpec(); - return project.abTests.map(t => t.name); + return (project.abTests ?? []).map(t => t.name); } catch { return []; } @@ -519,7 +519,7 @@ Target-Based Mode (--mode target-based) private async createABTest(options: AddABTestOptions): Promise { const project = await this.readProjectSpec(); - this.checkDuplicate(project.abTests, options.name); + this.checkDuplicate(project.abTests ?? [], options.name); // Resolve gateway reference based on the user's choice let gatewayRef: string; @@ -527,7 +527,7 @@ Target-Based Mode (--mode target-based) if (choice.type === 'existing-http') { // Reuse an existing HTTP gateway from the project spec - const existing = project.httpGateways.find(gw => gw.name === choice.name); + const existing = (project.httpGateways ?? []).find(gw => gw.name === choice.name); if (!existing) { throw new Error(`HTTP gateway "${choice.name}" not found in project.`); } @@ -535,7 +535,7 @@ Target-Based Mode (--mode target-based) } else { // Create new HTTP gateway — truncate name to fit 48-char limit const httpGwName = `${options.name.replace(/_/g, '-').slice(0, 44)}-gw`; - const existingGw = project.httpGateways.find(gw => gw.name === httpGwName); + const existingGw = (project.httpGateways ?? []).find(gw => gw.name === httpGwName); if (existingGw) { if (existingGw.runtimeRef !== options.agent) { throw new Error( @@ -544,6 +544,7 @@ Target-Based Mode (--mode target-based) ); } } else { + project.httpGateways ??= []; project.httpGateways.push({ name: httpGwName, runtimeRef: options.agent, @@ -590,6 +591,7 @@ Target-Based Mode (--mode target-based) ...(options.enableOnCreate !== undefined && { enableOnCreate: options.enableOnCreate }), }; + project.abTests ??= []; project.abTests.push(abTest); await this.writeProjectSpec(project); @@ -608,7 +610,7 @@ Target-Based Mode (--mode target-based) private async createTargetBasedABTest(options: AddTargetBasedABTestOptions): Promise { const project = await this.readProjectSpec(); - this.checkDuplicate(project.abTests, options.name); + this.checkDuplicate(project.abTests ?? [], options.name); // Validate runtime exists const runtime = project.runtimes.find(r => r.name === options.runtime); @@ -633,7 +635,7 @@ Target-Based Mode (--mode target-based) const treatmentTarget = `${options.runtime}-${options.treatmentEndpoint}`; // Auto-create HTTP gateway if it doesn't exist - let existing = project.httpGateways.find(gw => gw.name === options.gateway); + let existing = (project.httpGateways ?? []).find(gw => gw.name === options.gateway); if (!existing) { existing = { name: options.gateway, @@ -644,6 +646,7 @@ Target-Based Mode (--mode target-based) { name: treatmentTarget, runtimeRef: options.runtime, qualifier: options.treatmentEndpoint }, ], }; + project.httpGateways ??= []; project.httpGateways.push(existing); } else { // Gateway exists — ensure targets exist @@ -716,6 +719,7 @@ Target-Based Mode (--mode target-based) ...(options.enableOnCreate !== undefined && { enableOnCreate: options.enableOnCreate }), }; + project.abTests ??= []; project.abTests.push(abTest); await this.writeProjectSpec(project); diff --git a/src/cli/primitives/ConfigBundlePrimitive.ts b/src/cli/primitives/ConfigBundlePrimitive.ts index 1bb5678d1..26d76c4b7 100644 --- a/src/cli/primitives/ConfigBundlePrimitive.ts +++ b/src/cli/primitives/ConfigBundlePrimitive.ts @@ -45,12 +45,12 @@ export class ConfigBundlePrimitive extends BasePrimitive b.name === bundleName); + const index = (project.configBundles ?? []).findIndex(b => b.name === bundleName); if (index === -1) { return { success: false, error: `Configuration bundle "${bundleName}" not found.` }; } - project.configBundles.splice(index, 1); + project.configBundles!.splice(index, 1); await this.writeProjectSpec(project); return { success: true }; @@ -62,7 +62,7 @@ export class ConfigBundlePrimitive extends BasePrimitive { const project = await this.readProjectSpec(); - const bundle = project.configBundles.find(b => b.name === bundleName); + const bundle = (project.configBundles ?? []).find(b => b.name === bundleName); if (!bundle) { throw new Error(`Configuration bundle "${bundleName}" not found.`); } @@ -72,7 +72,7 @@ export class ConfigBundlePrimitive extends BasePrimitive b.name !== bundleName), + configBundles: (project.configBundles ?? []).filter(b => b.name !== bundleName), }; schemaChanges.push({ @@ -87,7 +87,7 @@ export class ConfigBundlePrimitive extends BasePrimitive { try { const project = await this.readProjectSpec(); - return project.configBundles.map(b => ({ name: b.name })); + return (project.configBundles ?? []).map(b => ({ name: b.name })); } catch { return []; } @@ -96,7 +96,7 @@ export class ConfigBundlePrimitive extends BasePrimitive { try { const project = await this.readProjectSpec(); - return project.configBundles.map(b => b.name); + return (project.configBundles ?? []).map(b => b.name); } catch { return []; } @@ -217,7 +217,7 @@ export class ConfigBundlePrimitive extends BasePrimitive { const project = await this.readProjectSpec(); - this.checkDuplicate(project.configBundles, options.name); + this.checkDuplicate(project.configBundles ?? [], options.name); const bundle: ConfigBundle = { name: options.name, @@ -228,6 +228,7 @@ export class ConfigBundlePrimitive extends BasePrimitive bundle.name, - name => `Duplicate config bundle name: ${name}` - ) - ), + .optional() + .superRefine((items, ctx) => { + if (items) { + uniqueBy( + (bundle: { name: string }) => bundle.name, + (name: string) => `Duplicate config bundle name: ${name}` + )(items, ctx); + } + }), abTests: z .array(ABTestSchema) - .default([]) - .superRefine( - uniqueBy( - test => test.name, - name => `Duplicate AB test name: ${name}` - ) - ), + .optional() + .superRefine((items, ctx) => { + if (items) { + uniqueBy( + (test: { name: string }) => test.name, + (name: string) => `Duplicate AB test name: ${name}` + )(items, ctx); + } + }), httpGateways: z .array(HttpGatewaySchema) - .default([]) - .superRefine( - uniqueBy( - gw => gw.name, - name => `Duplicate HTTP gateway name: ${name}` - ) - ), + .optional() + .superRefine((items, ctx) => { + if (items) { + uniqueBy( + (gw: { name: string }) => gw.name, + (name: string) => `Duplicate HTTP gateway name: ${name}` + )(items, ctx); + } + }), }) .strict() .superRefine((spec, ctx) => { @@ -381,7 +387,7 @@ export const AgentCoreProjectSpecSchema = z } // Validate HTTP gateway runtimeRef references - for (const gw of spec.httpGateways) { + for (const gw of spec.httpGateways ?? []) { const runtimeExists = spec.runtimes.some(r => r.name === gw.runtimeRef); if (!runtimeExists) { ctx.addIssue({ @@ -392,13 +398,13 @@ export const AgentCoreProjectSpecSchema = z } // Validate AB test gateway references - for (const test of spec.abTests) { + for (const test of spec.abTests ?? []) { const gwField = test.gatewayRef; if (gwField && typeof gwField === 'string') { const match = /^\{\{gateway:(.+)\}\}$/.exec(gwField); if (match) { const gwName = match[1]; - const gwExists = spec.httpGateways.some(gw => gw.name === gwName); + const gwExists = (spec.httpGateways ?? []).some(gw => gw.name === gwName); if (!gwExists) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -408,7 +414,7 @@ export const AgentCoreProjectSpecSchema = z // For target-based AB tests, validate target names exist in the gateway's targets array if (test.mode === 'target-based') { - const gw = spec.httpGateways.find(g => g.name === gwName); + const gw = (spec.httpGateways ?? []).find(g => g.name === gwName); if (gw) { const gwTargetNames = new Set((gw.targets ?? []).map(t => t.name)); for (const variant of test.variants) { @@ -427,7 +433,7 @@ export const AgentCoreProjectSpecSchema = z } // Validate HTTP gateway target runtimeRef and qualifier references - for (const gw of spec.httpGateways) { + for (const gw of spec.httpGateways ?? []) { for (const target of gw.targets ?? []) { const runtime = spec.runtimes.find(r => r.name === target.runtimeRef); if (!runtime) {