Skip to content
Merged
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
91 changes: 91 additions & 0 deletions integ-tests/add-remove-gateway.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
34 changes: 34 additions & 0 deletions src/cli/commands/add/__tests__/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ 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),
createExternalGatewayTarget: (...args: unknown[]) => mockCreateExternalGatewayTarget(...args),
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 = {
Expand Down Expand Up @@ -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');
});
});
102 changes: 19 additions & 83 deletions src/cli/commands/add/__tests__/add-gateway-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 () => {
Expand All @@ -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
Expand All @@ -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();
});
});
});
3 changes: 1 addition & 2 deletions src/cli/commands/add/__tests__/add-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
18 changes: 7 additions & 11 deletions src/cli/commands/remove/__tests__/remove-gateway-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -45,28 +44,25 @@ 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(
[
'add',
'gateway-target',
'--name',
tempTool,
'--language',
'Python',
'--endpoint',
'https://example.com/mcp',
'--gateway',
tempGateway,
'--host',
'Lambda',
'--json',
],
projectDir
Expand Down
29 changes: 21 additions & 8 deletions src/cli/commands/remove/__tests__/remove-gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading