From 51922f12f7010d862cde7c9c285aa797f88b8238 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Tue, 3 Mar 2026 15:00:26 -0500 Subject: [PATCH 1/3] fix: wire gateway-target CLI flags and default source to existing-endpoint - Pass source/type/endpoint options through command.tsx to handler (were silently dropped, breaking --source existing-endpoint) - Default source to 'existing-endpoint' in validation (only supported path) - Reject 'create-new' source with clear error message - Move outbound auth validation before source branching so it applies to existing-endpoint targets (was unreachable due to early return) --- src/cli/commands/add/command.tsx | 3 ++ src/cli/commands/add/validate.ts | 69 +++++++++++++++++--------------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/cli/commands/add/command.tsx b/src/cli/commands/add/command.tsx index 22e89dc5..8afd3982 100644 --- a/src/cli/commands/add/command.tsx +++ b/src/cli/commands/add/command.tsx @@ -120,6 +120,9 @@ async function handleAddGatewayTargetCLI(options: AddGatewayTargetOptions): Prom const result = await handleAddGatewayTarget({ name: options.name!, description: options.description, + type: options.type, + source: options.source as 'existing-endpoint' | 'create-new' | undefined, + endpoint: options.endpoint, language: options.language! as 'Python' | 'TypeScript', gateway: options.gateway, host: options.host, diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 0aac0a21..3f88c6c8 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -205,8 +205,8 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO return { valid: false, error: 'Invalid type. Valid options: mcpServer, lambda' }; } - if (options.source && options.source !== 'existing-endpoint' && options.source !== 'create-new') { - return { valid: false, error: 'Invalid source. Valid options: existing-endpoint, create-new' }; + if (options.source && options.source !== 'existing-endpoint') { + return { valid: false, error: "Only 'existing-endpoint' source is currently supported" }; } // Gateway is required — a gateway target must be attached to a gateway @@ -233,38 +233,10 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO }; } - if (options.source === 'existing-endpoint') { - if (options.host) { - return { valid: false, error: '--host is not applicable for existing endpoint targets' }; - } - if (!options.endpoint) { - return { valid: false, error: '--endpoint is required when source is existing-endpoint' }; - } - - try { - const url = new URL(options.endpoint); - if (url.protocol !== 'http:' && url.protocol !== 'https:') { - return { valid: false, error: 'Endpoint must use http:// or https:// protocol' }; - } - } catch { - return { valid: false, error: 'Endpoint must be a valid URL (e.g. https://example.com/mcp)' }; - } + // Default to existing-endpoint (only supported source for now) + options.source ??= 'existing-endpoint'; - // Populate defaults for fields skipped by external endpoint flow - options.language ??= 'Other'; - - return { valid: true }; - } - - if (!options.language) { - return { valid: false, error: '--language is required' }; - } - - if (options.language !== 'Python' && options.language !== 'TypeScript' && options.language !== 'Other') { - return { valid: false, error: 'Invalid language. Valid options: Python, TypeScript, Other' }; - } - - // Validate outbound auth configuration + // Validate outbound auth configuration (applies to all source types) if (options.outboundAuthType && options.outboundAuthType !== 'NONE') { const hasInlineOAuth = !!(options.oauthClientId ?? options.oauthClientSecret ?? options.oauthDiscoveryUrl); @@ -310,6 +282,37 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO } } + if (options.source === 'existing-endpoint') { + if (options.host) { + return { valid: false, error: '--host is not applicable for existing endpoint targets' }; + } + if (!options.endpoint) { + return { valid: false, error: '--endpoint is required when source is existing-endpoint' }; + } + + try { + const url = new URL(options.endpoint); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return { valid: false, error: 'Endpoint must use http:// or https:// protocol' }; + } + } catch { + return { valid: false, error: 'Endpoint must be a valid URL (e.g. https://example.com/mcp)' }; + } + + // Populate defaults for fields skipped by external endpoint flow + options.language ??= 'Other'; + + return { valid: true }; + } + + if (!options.language) { + return { valid: false, error: '--language is required' }; + } + + if (options.language !== 'Python' && options.language !== 'TypeScript' && options.language !== 'Other') { + return { valid: false, error: 'Invalid language. Valid options: Python, TypeScript, Other' }; + } + return { valid: true }; } From 0ecb51de04d15667878bca25e7bd301f8754307f Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Tue, 3 Mar 2026 15:00:49 -0500 Subject: [PATCH 2/3] test: unskip and update gateway tests for existing-endpoint path - Unskip all 4 gateway test files (add/remove gateway/gateway-target) - Update validGatewayTargetOptions fixture to existing-endpoint - Remove create-new path tests (not user-facing yet) - Add tests: rejects create-new source, requires endpoint, invalid OAuth discovery URL, auto-create OAuth credential - Rewrite cascade/removal tests to use --endpoint instead of non-existent 'add bind gateway' and --language/--host flags --- .../commands/add/__tests__/actions.test.ts | 34 ++++++ .../add/__tests__/add-gateway-target.test.ts | 102 ++++-------------- .../add/__tests__/add-gateway.test.ts | 3 +- .../commands/add/__tests__/validate.test.ts | 51 +++++---- .../__tests__/remove-gateway-target.test.ts | 18 ++-- .../remove/__tests__/remove-gateway.test.ts | 29 +++-- 6 files changed, 110 insertions(+), 127 deletions(-) diff --git a/src/cli/commands/add/__tests__/actions.test.ts b/src/cli/commands/add/__tests__/actions.test.ts index 0fffde89..c6566712 100644 --- a/src/cli/commands/add/__tests__/actions.test.ts +++ b/src/cli/commands/add/__tests__/actions.test.ts @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; const mockCreateToolFromWizard = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '/tmp' }); const mockCreateExternalGatewayTarget = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '' }); +const mockCreateCredential = vi.fn().mockResolvedValue(undefined); vi.mock('../../../operations/mcp/create-mcp', () => ({ createToolFromWizard: (...args: unknown[]) => mockCreateToolFromWizard(...args), @@ -11,6 +12,12 @@ vi.mock('../../../operations/mcp/create-mcp', () => ({ createGatewayFromWizard: vi.fn(), })); +vi.mock('../../../operations/identity/create-identity', () => ({ + createCredential: (...args: unknown[]) => mockCreateCredential(...args), + computeDefaultCredentialEnvVarName: vi.fn(), + resolveCredentialStrategy: vi.fn(), +})); + describe('buildGatewayTargetConfig', () => { it('maps name, gateway, language correctly', () => { const options: ValidatedAddGatewayTargetOptions = { @@ -97,4 +104,31 @@ describe('handleAddGatewayTarget', () => { expect(mockCreateExternalGatewayTarget).toHaveBeenCalledOnce(); expect(mockCreateToolFromWizard).not.toHaveBeenCalled(); }); + + it('auto-creates OAuth credential when inline fields provided', async () => { + const options: ValidatedAddGatewayTargetOptions = { + name: 'my-tool', + language: 'Other', + host: 'Lambda', + source: 'existing-endpoint', + endpoint: 'https://example.com/mcp', + gateway: 'my-gw', + oauthClientId: 'cid', + oauthClientSecret: 'csec', + oauthDiscoveryUrl: 'https://auth.example.com', + oauthScopes: 'read,write', + }; + + await handleAddGatewayTarget(options); + + expect(mockCreateCredential).toHaveBeenCalledWith({ + type: 'OAuthCredentialProvider', + name: 'my-tool-oauth', + discoveryUrl: 'https://auth.example.com', + clientId: 'cid', + clientSecret: 'csec', + scopes: ['read', 'write'], + }); + expect(options.credentialName).toBe('my-tool-oauth'); + }); }); diff --git a/src/cli/commands/add/__tests__/add-gateway-target.test.ts b/src/cli/commands/add/__tests__/add-gateway-target.test.ts index bbec3694..ef8820bb 100644 --- a/src/cli/commands/add/__tests__/add-gateway-target.test.ts +++ b/src/cli/commands/add/__tests__/add-gateway-target.test.ts @@ -5,8 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -// Gateway Target feature is disabled (coming soon) - skip all tests -describe.skip('add gateway-target command', () => { +describe('add gateway-target command', () => { let testDir: string; let projectDir: string; const gatewayName = 'test-gateway'; @@ -22,6 +21,12 @@ describe.skip('add gateway-target command', () => { throw new Error(`Failed to create project: ${result.stdout} ${result.stderr}`); } projectDir = join(testDir, projectName); + + // Create gateway for tests + const gwResult = await runCLI(['add', 'gateway', '--name', gatewayName, '--json'], projectDir); + if (gwResult.exitCode !== 0) { + throw new Error(`Failed to create gateway: ${gwResult.stdout} ${gwResult.stderr}`); + } }); afterAll(async () => { @@ -37,53 +42,31 @@ describe.skip('add gateway-target command', () => { expect(json.error.includes('--name'), `Error: ${json.error}`).toBeTruthy(); }); - it('validates language', async () => { + it('requires endpoint', async () => { const result = await runCLI( - ['add', 'gateway-target', '--name', 'test', '--language', 'InvalidLang', '--json'], + ['add', 'gateway-target', '--name', 'noendpoint', '--gateway', gatewayName, '--json'], projectDir ); expect(result.exitCode).toBe(1); const json = JSON.parse(result.stdout); expect(json.success).toBe(false); - expect( - json.error.toLowerCase().includes('invalid') || json.error.toLowerCase().includes('valid options'), - `Error should mention invalid language: ${json.error}` - ).toBeTruthy(); - }); - - it('accepts Other as valid language option', async () => { - const result = await runCLI( - ['add', 'gateway-target', '--name', 'container-tool', '--language', 'Other', '--json'], - projectDir - ); - - // Should fail with "not yet supported" error, not validation error - expect(result.exitCode).toBe(1); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - expect( - json.error.toLowerCase().includes('not yet supported') || json.error.toLowerCase().includes('other'), - `Error should mention Other not supported: ${json.error}` - ).toBeTruthy(); + expect(json.error.includes('--endpoint'), `Error: ${json.error}`).toBeTruthy(); }); }); - // Gateway disabled - skip behind-gateway tests until gateway feature is enabled - describe.skip('behind-gateway', () => { - it('creates behind-gateway tool', async () => { - const toolName = `gwtool${Date.now()}`; + describe('existing-endpoint', () => { + it('adds existing-endpoint target to gateway', async () => { + const targetName = `target${Date.now()}`; const result = await runCLI( [ 'add', 'gateway-target', '--name', - toolName, - '--language', - 'Python', + targetName, + '--endpoint', + 'https://mcp.exa.ai/mcp', '--gateway', gatewayName, - '--host', - 'Lambda', '--json', ], projectDir @@ -92,59 +75,12 @@ describe.skip('add gateway-target command', () => { expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - expect(json.toolName).toBe(toolName); - // Verify in mcp.json gateway targets + // Verify in mcp.json const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); - const target = gateway?.targets?.find((t: { name: string }) => t.name === toolName); - expect(target, 'Tool should be in gateway targets').toBeTruthy(); - }); - - it('requires gateway for behind-gateway', async () => { - const result = await runCLI( - ['add', 'gateway-target', '--name', 'no-gw', '--language', 'Python', '--host', 'Lambda', '--json'], - projectDir - ); - expect(result.exitCode).toBe(1); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - expect(json.error.includes('--gateway'), `Error: ${json.error}`).toBeTruthy(); - }); - - it('requires host for behind-gateway', async () => { - const result = await runCLI( - ['add', 'gateway-target', '--name', 'no-host', '--language', 'Python', '--gateway', gatewayName, '--json'], - projectDir - ); - expect(result.exitCode).toBe(1); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - expect(json.error.includes('--host'), `Error: ${json.error}`).toBeTruthy(); - }); - - it('returns clear error for Other language with behind-gateway', async () => { - const result = await runCLI( - [ - 'add', - 'gateway-target', - '--name', - 'gateway-container', - '--language', - 'Other', - '--gateway', - gatewayName, - '--host', - 'Lambda', - '--json', - ], - projectDir - ); - - expect(result.exitCode).toBe(1); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - expect(json.error.length > 0, 'Should have error message').toBeTruthy(); + const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); + expect(target, 'Target should be in gateway targets').toBeTruthy(); }); }); }); diff --git a/src/cli/commands/add/__tests__/add-gateway.test.ts b/src/cli/commands/add/__tests__/add-gateway.test.ts index 3f1d114f..91c17100 100644 --- a/src/cli/commands/add/__tests__/add-gateway.test.ts +++ b/src/cli/commands/add/__tests__/add-gateway.test.ts @@ -5,8 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -// Gateway disabled - skip until gateway feature is enabled -describe.skip('add gateway command', () => { +describe('add gateway command', () => { let testDir: string; let projectDir: string; diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index 0d4f7961..82547d9a 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -61,9 +61,9 @@ const validGatewayOptionsJwt: AddGatewayOptions = { const validGatewayTargetOptions: AddGatewayTargetOptions = { name: 'test-tool', - language: 'Python', + source: 'existing-endpoint', + endpoint: 'https://example.com/mcp', gateway: 'my-gateway', - host: 'Lambda', }; const validMemoryOptions: AddMemoryOptions = { @@ -297,14 +297,6 @@ describe('validate', () => { expect(result.error).toBe('--name is required'); }); - it('returns error for missing language (non-existing-endpoint)', async () => { - const opts = { ...validGatewayTargetOptions, language: undefined }; - const result = await validateAddGatewayTargetOptions(opts); - expect(result.valid).toBe(false); - expect(result.error).toBe('--language is required'); - }); - - // Gateway is required it('returns error when --gateway is missing', async () => { const opts = { ...validGatewayTargetOptions, gateway: undefined }; const result = await validateAddGatewayTargetOptions(opts); @@ -328,22 +320,23 @@ describe('validate', () => { expect(result.error).toContain('other-gateway'); }); - // AC16: Invalid values rejected - it('returns error for invalid values', async () => { - const result = await validateAddGatewayTargetOptions({ - ...validGatewayTargetOptions, - language: 'Java' as any, - }); - expect(result.valid).toBe(false); - expect(result.error?.includes('Invalid language')).toBeTruthy(); - }); - // AC18: Valid options pass it('passes for valid gateway target options', async () => { const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptions }); expect(result.valid).toBe(true); }); // AC20: existing-endpoint source validation + it('rejects create-new source', async () => { + const options: AddGatewayTargetOptions = { + name: 'test-tool', + source: 'create-new' as any, + gateway: 'my-gateway', + }; + const result = await validateAddGatewayTargetOptions(options); + expect(result.valid).toBe(false); + expect(result.error).toBe("Only 'existing-endpoint' source is currently supported"); + }); + it('passes for valid existing-endpoint with https', async () => { const options: AddGatewayTargetOptions = { name: 'test-tool', @@ -410,7 +403,7 @@ describe('validate', () => { const options: AddGatewayTargetOptions = { name: 'test-tool', - language: 'Python', + endpoint: 'https://example.com/mcp', gateway: 'my-gateway', outboundAuthType: 'API_KEY', credentialName: 'missing-cred', @@ -427,7 +420,7 @@ describe('validate', () => { const options: AddGatewayTargetOptions = { name: 'test-tool', - language: 'Python', + endpoint: 'https://example.com/mcp', gateway: 'my-gateway', outboundAuthType: 'API_KEY', credentialName: 'any-cred', @@ -444,7 +437,7 @@ describe('validate', () => { const options: AddGatewayTargetOptions = { name: 'test-tool', - language: 'Python', + endpoint: 'https://example.com/mcp', gateway: 'my-gateway', outboundAuthType: 'API_KEY', credentialName: 'valid-cred', @@ -506,6 +499,18 @@ describe('validate', () => { expect(result.error).toContain('--credential-name is required'); }); + it('returns error for invalid OAuth discovery URL', async () => { + const result = await validateAddGatewayTargetOptions({ + ...validGatewayTargetOptions, + outboundAuthType: 'OAUTH', + oauthClientId: 'cid', + oauthClientSecret: 'csec', + oauthDiscoveryUrl: 'not-a-url', + }); + expect(result.valid).toBe(false); + expect(result.error).toBe('--oauth-discovery-url must be a valid URL'); + }); + it('rejects --host with existing-endpoint', async () => { const options: AddGatewayTargetOptions = { name: 'test-tool', diff --git a/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts b/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts index 20b65e1e..67130423 100644 --- a/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts +++ b/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts @@ -5,8 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -// Gateway Target feature is disabled (coming soon) - skip all tests -describe.skip('remove gateway-target command', () => { +describe('remove gateway-target command', () => { let testDir: string; let projectDir: string; @@ -45,15 +44,14 @@ describe.skip('remove gateway-target command', () => { }); }); - // Gateway disabled - skip behind-gateway tests until gateway feature is enabled - describe.skip('remove behind-gateway tool', () => { - it('removes behind-gateway tool from gateway targets', async () => { - // Create a fresh gateway for this test to avoid conflicts with existing tools + describe('remove existing-endpoint target', () => { + it('removes target from gateway', async () => { + // Create a fresh gateway const tempGateway = `TempGw${Date.now()}`; const gwResult = await runCLI(['add', 'gateway', '--name', tempGateway, '--json'], projectDir); expect(gwResult.exitCode, `gateway add failed: ${gwResult.stdout}`).toBe(0); - // Add a tool to the fresh gateway + // Add a target to the gateway const tempTool = `tempTool${Date.now()}`; const addResult = await runCLI( [ @@ -61,12 +59,10 @@ describe.skip('remove gateway-target command', () => { 'gateway-target', '--name', tempTool, - '--language', - 'Python', + '--endpoint', + 'https://example.com/mcp', '--gateway', tempGateway, - '--host', - 'Lambda', '--json', ], projectDir diff --git a/src/cli/commands/remove/__tests__/remove-gateway.test.ts b/src/cli/commands/remove/__tests__/remove-gateway.test.ts index 51516ffd..a2d273e2 100644 --- a/src/cli/commands/remove/__tests__/remove-gateway.test.ts +++ b/src/cli/commands/remove/__tests__/remove-gateway.test.ts @@ -5,8 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -// Gateway disabled - skip until gateway feature is enabled -describe.skip('remove gateway command', () => { +describe('remove gateway command', () => { let testDir: string; let projectDir: string; const gatewayName = 'TestGateway'; @@ -93,15 +92,29 @@ describe.skip('remove gateway command', () => { expect(!gateway, 'Gateway should be removed').toBeTruthy(); }); - it('removes gateway with attached agents using cascade policy (default)', async () => { - // Bind gateway to agent - const bindResult = await runCLI( - ['add', 'bind', 'gateway', '--agent', agentName, '--gateway', gatewayName, '--name', 'GatewayTool', '--json'], + it('removes gateway with targets attached', async () => { + // Re-add gateway since previous test may have removed it + await runCLI(['add', 'gateway', '--name', gatewayName, '--json'], projectDir); + + // Add a target to the gateway + const targetName = `target${Date.now()}`; + const addResult = await runCLI( + [ + 'add', + 'gateway-target', + '--name', + targetName, + '--endpoint', + 'https://example.com/mcp', + '--gateway', + gatewayName, + '--json', + ], projectDir ); - expect(bindResult.exitCode, `bind failed: ${bindResult.stdout}`).toBe(0); + expect(addResult.exitCode, `add target failed: ${addResult.stdout}`).toBe(0); - // Remove with cascade policy (default) - should succeed and clean up references + // Remove gateway - should succeed and clean up targets const result = await runCLI(['remove', 'gateway', '--name', gatewayName, '--json'], projectDir); expect(result.exitCode, `stdout: ${result.stdout}`).toBe(0); const json = JSON.parse(result.stdout); From 3b079e95610dec24e0fd8538a771cc4a44054465 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Tue, 3 Mar 2026 15:01:24 -0500 Subject: [PATCH 3/3] test: add gateway integration test and gateway-env unit tests - Add end-to-end gateway lifecycle test (add gateway, add target with Exa MCP endpoint, remove target, remove gateway) - Add unit tests for getGatewayEnvVars (dev environment setup) --- integ-tests/add-remove-gateway.test.ts | 91 +++++++++++++++++++ .../dev/__tests__/gateway-env.test.ts | 91 +++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 integ-tests/add-remove-gateway.test.ts create mode 100644 src/cli/operations/dev/__tests__/gateway-env.test.ts diff --git a/integ-tests/add-remove-gateway.test.ts b/integ-tests/add-remove-gateway.test.ts new file mode 100644 index 00000000..88d64cb0 --- /dev/null +++ b/integ-tests/add-remove-gateway.test.ts @@ -0,0 +1,91 @@ +import { createTestProject, runCLI } from '../src/test-utils/index.js'; +import type { TestProject } from '../src/test-utils/index.js'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +async function readMcpConfig(projectPath: string) { + return JSON.parse(await readFile(join(projectPath, 'agentcore/mcp.json'), 'utf-8')); +} + +describe('integration: add and remove gateway with external MCP server', () => { + let project: TestProject; + const gatewayName = 'ExaGateway'; + const targetName = 'ExaSearch'; + + beforeAll(async () => { + project = await createTestProject({ noAgent: true }); + }); + + afterAll(async () => { + await project.cleanup(); + }); + + describe('gateway lifecycle', () => { + it('adds a gateway', async () => { + const result = await runCLI(['add', 'gateway', '--name', gatewayName, '--json'], project.projectPath); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + const mcpSpec = await readMcpConfig(project.projectPath); + const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); + expect(gateway, `Gateway "${gatewayName}" should be in mcp.json`).toBeTruthy(); + expect(gateway.authorizerType).toBe('NONE'); + }); + + it('adds an external MCP server target to the gateway', async () => { + const result = await runCLI( + [ + 'add', + 'gateway-target', + '--name', + targetName, + '--endpoint', + 'https://mcp.exa.ai/mcp', + '--gateway', + gatewayName, + '--json', + ], + project.projectPath + ); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + const mcpSpec = await readMcpConfig(project.projectPath); + const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); + const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); + expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy(); + }); + + it('removes the gateway target', async () => { + const result = await runCLI(['remove', 'gateway-target', '--name', targetName, '--json'], project.projectPath); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + const mcpSpec = await readMcpConfig(project.projectPath); + const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); + const targets = gateway?.targets ?? []; + const found = targets.find((t: { name: string }) => t.name === targetName); + expect(found, `Target "${targetName}" should be removed`).toBeFalsy(); + }); + + it('removes the gateway', async () => { + const result = await runCLI(['remove', 'gateway', '--name', gatewayName, '--json'], project.projectPath); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + const mcpSpec = await readMcpConfig(project.projectPath); + const gateways = mcpSpec.agentCoreGateways ?? []; + const found = gateways.find((g: { name: string }) => g.name === gatewayName); + expect(found, `Gateway "${gatewayName}" should be removed`).toBeFalsy(); + }); + }); +}); diff --git a/src/cli/operations/dev/__tests__/gateway-env.test.ts b/src/cli/operations/dev/__tests__/gateway-env.test.ts new file mode 100644 index 00000000..d880b356 --- /dev/null +++ b/src/cli/operations/dev/__tests__/gateway-env.test.ts @@ -0,0 +1,91 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockReadDeployedState, mockReadMcpSpec, mockConfigExists } = vi.hoisted(() => ({ + mockReadDeployedState: vi.fn(), + mockReadMcpSpec: vi.fn(), + mockConfigExists: vi.fn(), +})); + +vi.mock('../../../../lib/index.js', () => ({ + ConfigIO: class { + readDeployedState = mockReadDeployedState; + readMcpSpec = mockReadMcpSpec; + configExists = mockConfigExists; + }, +})); + +const { getGatewayEnvVars } = await import('../gateway-env.js'); + +describe('getGatewayEnvVars', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns empty when no deployed state', async () => { + mockReadDeployedState.mockRejectedValue(new Error('not found')); + const result = await getGatewayEnvVars(); + expect(result).toEqual({}); + }); + + it('returns empty when no gateways deployed', async () => { + mockReadDeployedState.mockResolvedValue({ targets: {} }); + mockConfigExists.mockReturnValue(false); + const result = await getGatewayEnvVars(); + expect(result).toEqual({}); + }); + + it('generates URL and AUTH_TYPE env vars for deployed gateway', async () => { + mockReadDeployedState.mockResolvedValue({ + targets: { + default: { + resources: { + mcp: { + gateways: { + 'my-gateway': { gatewayUrl: 'https://gw.example.com' }, + }, + }, + }, + }, + }, + }); + mockConfigExists.mockReturnValue(true); + mockReadMcpSpec.mockResolvedValue({ + agentCoreGateways: [{ name: 'my-gateway', authorizerType: 'CUSTOM_JWT' }], + }); + + const result = await getGatewayEnvVars(); + expect(result).toEqual({ + AGENTCORE_GATEWAY_MY_GATEWAY_URL: 'https://gw.example.com', + AGENTCORE_GATEWAY_MY_GATEWAY_AUTH_TYPE: 'CUSTOM_JWT', + }); + }); + + it('defaults auth type to NONE when gateway not in mcp spec', async () => { + mockReadDeployedState.mockResolvedValue({ + targets: { + default: { + resources: { mcp: { gateways: { 'test-gw': { gatewayUrl: 'https://test.com' } } } }, + }, + }, + }); + mockConfigExists.mockReturnValue(true); + mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [] }); + + const result = await getGatewayEnvVars(); + expect(result.AGENTCORE_GATEWAY_TEST_GW_AUTH_TYPE).toBe('NONE'); + }); + + it('skips gateways without gatewayUrl', async () => { + mockReadDeployedState.mockResolvedValue({ + targets: { + default: { + resources: { mcp: { gateways: { 'no-url': {} } } }, + }, + }, + }); + mockConfigExists.mockReturnValue(false); + + const result = await getGatewayEnvVars(); + expect(result).toEqual({}); + }); +});