Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e5823f2
feat: add gateway import command and unhide import from TUI
jesseturner21 Apr 14, 2026
02f7cd3
fix: expand ARN input to show full resource ARN and add gateway support
jesseturner21 Apr 14, 2026
d2671e8
refactor: remove --name and --yes flags from import gateway command
jesseturner21 Apr 14, 2026
916c10e
feat: add e2e tests for import gateway command
jesseturner21 Apr 14, 2026
631e235
chore: gitignore bugbash-resources.json and .omc/
jesseturner21 Apr 14, 2026
9a0a571
feat: preserve gateway executionRoleArn during import
jesseturner21 Apr 15, 2026
9bdc60a
Merge branch 'main' into feat/import-gateway
jesseturner21 Apr 15, 2026
b09a133
refactor: export internal gateway import functions for unit testing
jesseturner21 Apr 16, 2026
6b56208
test: add unit tests for mcpServer target mapping and credential reso…
jesseturner21 Apr 16, 2026
53599c5
test: add unit tests for apiGateway, openApiSchema, smithyModel, lamb…
jesseturner21 Apr 16, 2026
0f36496
test: add unit tests for toGatewaySpec gateway-level field mapping
jesseturner21 Apr 16, 2026
d95937c
test: add unit tests for handleImportGateway full flow validation
jesseturner21 Apr 16, 2026
435fa22
test: add unit tests for buildCredentialArnMap and CFN template matching
jesseturner21 Apr 16, 2026
f518b15
fix: exclude already-deployed logical IDs when building import resour…
jesseturner21 Apr 21, 2026
d314a72
fix(import): translate AccessDenied on GetGateway to a friendly not-f…
jesseturner21 Apr 21, 2026
4932cd9
fix(import): detect AWS_REGION / ARN region mismatch before import
jesseturner21 Apr 21, 2026
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,9 @@ ProtocolTesting/

# Auto-cloned CDK constructs (from scripts/bundle.mjs)
.cdk-constructs-clone/

# E2E test artifacts
e2e-tests/fixtures/import/bugbash-resources.json

# oh-my-claudecode
.omc/
6 changes: 6 additions & 0 deletions e2e-tests/fixtures/import/cleanup_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,19 @@ def main():
rid = val.get("id")
if not rid:
continue
# Gateway targets are deleted automatically when the parent gateway is deleted
if "gateway" in key and "target" in key:
print(f"Skipping {key} (deleted with parent gateway)")
continue
try:
if "runtime" in key:
client.delete_agent_runtime(agentRuntimeId=rid)
elif "memory" in key:
client.delete_memory(memoryId=rid)
elif "evaluator" in key:
client.delete_evaluator(evaluatorId=rid)
elif "gateway" in key:
client.delete_gateway(gatewayIdentifier=rid)
print(f"Deleted {key}: {rid}")
except Exception as e:
print(f"Could not delete {key} ({rid}): {e}")
Expand Down
45 changes: 45 additions & 0 deletions e2e-tests/fixtures/import/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,48 @@ def tag_resource(client, arn, tags):
"""Tag a resource via the control plane API."""
print(f"Tagging resource with {tags}...")
client.tag_resource(resourceArn=arn, tags=tags)


def wait_for_gateway(client, gateway_id, timeout=120):
"""Wait for a gateway to reach READY status."""
print(f"Waiting for gateway {gateway_id} to become READY...")
start = time.time()
while time.time() - start < timeout:
resp = client.get_gateway(gatewayIdentifier=gateway_id)
status = resp.get("status", "UNKNOWN")
if status == "READY":
print(f"Gateway {gateway_id} is READY")
return True
if status in ("CREATE_FAILED", "FAILED"):
reason = resp.get("statusReasons", [{}])
print(f"ERROR: Gateway {gateway_id} status: {status} — {reason}")
return False
elapsed = int(time.time() - start)
print(f" Status: {status} ({elapsed}s elapsed)")
time.sleep(5)
print(f"WARNING: Gateway did not reach READY after {timeout}s")
return False


def wait_for_gateway_target(client, gateway_id, target_id, timeout=120):
"""Wait for a gateway target to reach READY status."""
print(f"Waiting for target {target_id} to become READY...")
start = time.time()
while time.time() - start < timeout:
resp = client.get_gateway_target(
gatewayIdentifier=gateway_id,
targetId=target_id,
)
status = resp.get("status", "UNKNOWN")
if status == "READY":
print(f"Target {target_id} is READY")
return True
if status in ("CREATE_FAILED", "FAILED"):
reason = resp.get("statusReasons", [{}])
print(f"ERROR: Target {target_id} status: {status} — {reason}")
return False
elapsed = int(time.time() - start)
print(f" Status: {status} ({elapsed}s elapsed)")
time.sleep(5)
print(f"WARNING: Target did not reach READY after {timeout}s")
return False
88 changes: 88 additions & 0 deletions e2e-tests/fixtures/import/setup_gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Setup: Gateway with MCP server target + tags.

Tests: gateway import, target mapping, authorizerType, enableSemanticSearch,
exceptionLevel, tags, deployed state nesting under mcp.gateways.

Creates:
1. A gateway with NONE authorizer + semantic search enabled
2. An MCP Server target pointing to a public test endpoint
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import time
from common import (
REGION, get_control_client, ensure_role, save_resource,
tag_resource, wait_for_gateway, wait_for_gateway_target,
)


def main():
role_arn = ensure_role()
client = get_control_client()
ts = int(time.time())
gateway_name = f"bugbashGw{ts}"

# ------------------------------------------------------------------
# 1. Create gateway
# ------------------------------------------------------------------
print(f"Creating gateway: {gateway_name}")
resp = client.create_gateway(
name=gateway_name,
description="Bugbash gateway for import testing",
roleArn=role_arn,
protocolType="MCP",
protocolConfiguration={
"mcp": {
"supportedVersions": ["2025-03-26"],
"searchType": "SEMANTIC",
},
},
authorizerType="NONE",
exceptionLevel="DEBUG",
)

gateway_id = resp["gatewayId"]
gateway_arn = resp["gatewayArn"]
print(f"Gateway ID: {gateway_id}")
print(f"Gateway ARN: {gateway_arn}")

tag_resource(client, gateway_arn, {
"env": "bugbash",
"team": "agentcore-cli",
})

save_resource("gateway", gateway_arn, gateway_id)

if not wait_for_gateway(client, gateway_id):
print("Gateway creation failed. Aborting target creation.")
sys.exit(1)

# ------------------------------------------------------------------
# 2. Create MCP Server target
# ------------------------------------------------------------------
target_name = "mcpTarget"
print(f"\nCreating MCP Server target: {target_name}")
target_resp = client.create_gateway_target(
gatewayIdentifier=gateway_id,
name=target_name,
targetConfiguration={
"mcp": {
"mcpServer": {
"endpoint": "https://mcp.exa.ai/mcp",
},
},
},
)

target_id = target_resp["targetId"]
print(f"Target ID: {target_id}")

save_resource("gateway-target-mcp", gateway_arn, target_id)
wait_for_gateway_target(client, gateway_id, target_id)


if __name__ == "__main__":
main()
89 changes: 87 additions & 2 deletions e2e-tests/import-resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const hasPython =
})();
const canRun = prereqs.npm && prereqs.git && prereqs.uv && hasAws && hasPython;

describe.sequential('e2e: import runtime/memory/evaluator', () => {
describe.sequential('e2e: import runtime/memory/evaluator/gateway', () => {
const region = process.env.AWS_REGION ?? 'us-east-1';
const fixtureDir = join(__dirname, 'fixtures', 'import');
const appDir = join(fixtureDir, 'app');
Expand All @@ -40,6 +40,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => {
let runtimeArn: string;
let memoryArn: string;
let evaluatorArn: string;
let gatewayArn: string;
let projectPath: string;
let testDir: string;

Expand All @@ -50,7 +51,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => {
// Each script creates a resource and saves its ARN/ID to bugbash-resources.json.
// Scripts run sequentially because save_resource() does a read-modify-write
// on a shared bugbash-resources.json file — parallel runs would race.
for (const script of ['setup_runtime_basic.py', 'setup_memory_full.py', 'setup_evaluator.py']) {
for (const script of ['setup_runtime_basic.py', 'setup_memory_full.py', 'setup_evaluator.py', 'setup_gateway.py']) {
const result = await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', script], fixtureDir, {
AWS_REGION: region,
DEFAULT_EVALUATOR_MODEL,
Expand All @@ -68,6 +69,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => {
runtimeArn = resources['runtime-basic']!.arn;
memoryArn = resources['memory-full']!.arn;
evaluatorArn = resources['evaluator-llm']!.arn;
gatewayArn = resources.gateway!.arn;

// 3. Create a destination CLI project (no agent — we'll import one)
testDir = join(tmpdir(), `agentcore-e2e-import-${randomUUID()}`);
Expand Down Expand Up @@ -163,6 +165,22 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => {
600_000
);

it.skipIf(!canRun)(
'imports a gateway by ARN',
async () => {
const result = await run(['import', 'gateway', '--arn', gatewayArn]);

if (result.exitCode !== 0) {
console.log('Import gateway stdout:', result.stdout);
console.log('Import gateway stderr:', result.stderr);
}

expect(result.exitCode, `Import gateway failed: ${result.stderr}`).toBe(0);
expect(stripAnsi(result.stdout).toLowerCase()).toContain('imported successfully');
},
600_000
);

// ── Verification tests ────────────────────────────────────────────

it.skipIf(!canRun)(
Expand All @@ -187,6 +205,73 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => {

const evaluator = json.resources.find(r => r.resourceType === 'evaluator');
expect(evaluator, 'Imported evaluator should appear in status').toBeDefined();

const gateway = json.resources.find(r => r.resourceType === 'gateway');
expect(gateway, 'Imported gateway should appear in status').toBeDefined();
},
120_000
);

it.skipIf(!canRun)(
'agentcore.json has correct gateway fields',
async () => {
const configPath = join(projectPath, 'agentcore', 'agentcore.json');
const config = JSON.parse(await readFile(configPath, 'utf-8')) as {
agentCoreGateways: {
name: string;
resourceName?: string;
description?: string;
authorizerType: string;
enableSemanticSearch: boolean;
exceptionLevel: string;
executionRoleArn?: string;
tags?: Record<string, string>;
targets: { name: string; targetType: string; endpoint?: string }[];
}[];
};

expect(config.agentCoreGateways.length, 'Should have one gateway').toBe(1);
const gw = config.agentCoreGateways[0]!;

expect(gw.name, 'Gateway name should be set').toBeTruthy();
expect(gw.resourceName, 'resourceName should preserve AWS name').toBeTruthy();
expect(gw.description).toBe('Bugbash gateway for import testing');
expect(gw.authorizerType).toBe('NONE');
expect(gw.enableSemanticSearch).toBe(true);
expect(gw.exceptionLevel).toBe('DEBUG');
expect(gw.tags).toEqual({ env: 'bugbash', team: 'agentcore-cli' });

expect(gw.executionRoleArn, 'executionRoleArn should be preserved from AWS').toBeTruthy();
expect(gw.executionRoleArn).toContain('bugbash-agentcore-role');

expect(gw.targets.length, 'Should have one target').toBe(1);
expect(gw.targets[0]!.name).toBe('mcpTarget');
expect(gw.targets[0]!.targetType).toBe('mcpServer');
expect(gw.targets[0]!.endpoint).toBe('https://mcp.exa.ai/mcp');
},
120_000
);

it.skipIf(!canRun)(
'deployed-state.json has gateway entry',
async () => {
const statePath = join(projectPath, 'agentcore', '.cli', 'deployed-state.json');
const state = JSON.parse(await readFile(statePath, 'utf-8')) as Record<string, unknown>;

// Gateway state is stored under targets.<targetName>.resources.mcp.gateways
const targets = state.targets as Record<string, { resources?: { mcp?: { gateways?: Record<string, unknown> } } }>;
const targetEntries = Object.values(targets);
expect(targetEntries.length).toBeGreaterThan(0);

const firstTarget = targetEntries[0]!;
const gateways = firstTarget.resources?.mcp?.gateways;
expect(gateways, 'deployed-state should have mcp.gateways entry').toBeDefined();

const gatewayEntries = Object.values(gateways!);
expect(gatewayEntries.length, 'Should have one gateway in deployed state').toBe(1);

const gwState = gatewayEntries[0] as { gatewayId?: string; gatewayArn?: string };
expect(gwState.gatewayId, 'Gateway ID should be recorded').toBeTruthy();
},
120_000
);
Expand Down
Loading
Loading