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
37 changes: 37 additions & 0 deletions e2e-tests/e2e-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {
BedrockAgentCoreControlClient,
DeleteApiKeyCredentialProviderCommand,
GetAgentRuntimeCommand,
} from '@aws-sdk/client-bedrock-agentcore-control';
import { execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
Expand All @@ -26,6 +27,11 @@ interface E2EConfig {
requiredEnvVar?: string;
build?: string;
memory?: string;
/** Lifecycle configuration to pass via --idle-timeout / --max-lifetime flags. */
lifecycleConfig?: {
idleTimeout?: number;
maxLifetime?: number;
};
}

export function createE2ESuite(cfg: E2EConfig) {
Expand Down Expand Up @@ -63,6 +69,13 @@ export function createE2ESuite(cfg: E2EConfig) {
createArgs.push('--build', cfg.build);
}

if (cfg.lifecycleConfig?.idleTimeout !== undefined) {
createArgs.push('--idle-timeout', String(cfg.lifecycleConfig.idleTimeout));
}
if (cfg.lifecycleConfig?.maxLifetime !== undefined) {
createArgs.push('--max-lifetime', String(cfg.lifecycleConfig.maxLifetime));
}

// Pass API key so the credential is registered in the project and .env.local
const apiKey = cfg.requiredEnvVar ? process.env[cfg.requiredEnvVar] : undefined;
if (apiKey) {
Expand Down Expand Up @@ -262,6 +275,30 @@ export function createE2ESuite(cfg: E2EConfig) {
},
120000
);

// ── Lifecycle configuration verification ─────────────────────────
if (cfg.lifecycleConfig) {
it.skipIf(!canRun)(
'runtime has lifecycle configuration set via AWS API',
async () => {
expect(runtimeId, 'Runtime ID should have been extracted from status').toBeTruthy();

// Query the runtime via AWS API to verify lifecycle config
const region = process.env.AWS_REGION ?? 'us-east-1';
const client = new BedrockAgentCoreControlClient({ region });
const response = await client.send(new GetAgentRuntimeCommand({ agentRuntimeId: runtimeId }));

expect(response.lifecycleConfiguration).toBeDefined();
if (cfg.lifecycleConfig!.idleTimeout !== undefined) {
expect(response.lifecycleConfiguration!.idleRuntimeSessionTimeout).toBe(cfg.lifecycleConfig!.idleTimeout);
}
if (cfg.lifecycleConfig!.maxLifetime !== undefined) {
expect(response.lifecycleConfiguration!.maxLifetime).toBe(cfg.lifecycleConfig!.maxLifetime);
}
},
180000
);
}
});
}

Expand Down
5 changes: 4 additions & 1 deletion e2e-tests/strands-bedrock.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { createE2ESuite } from './e2e-helper.js';

createE2ESuite({ framework: 'Strands', modelProvider: 'Bedrock' });
createE2ESuite({
framework: 'Strands',
modelProvider: 'Bedrock',
});
322 changes: 322 additions & 0 deletions integ-tests/lifecycle-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
import { readProjectConfig, runCLI } from '../src/test-utils/index.js';
import { randomUUID } from 'node:crypto';
import { mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

describe('integration: lifecycle configuration', () => {
let testDir: string;
let projectPath: string;

beforeAll(async () => {
testDir = join(tmpdir(), `agentcore-integ-lifecycle-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const result = await runCLI(['create', '--name', 'LifecycleTest', '--no-agent', '--json'], testDir);
expect(result.exitCode, `setup stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
projectPath = json.projectPath;
});

afterAll(async () => {
await rm(testDir, { recursive: true, force: true });
});

describe('create with lifecycle flags', () => {
let createDir: string;

beforeAll(async () => {
createDir = join(tmpdir(), `agentcore-integ-lifecycle-create-${randomUUID()}`);
await mkdir(createDir, { recursive: true });
});

afterAll(async () => {
await rm(createDir, { recursive: true, force: true });
});

it('creates project with --idle-timeout and --max-lifetime', async () => {
const name = `LcCreate${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'create',
'--name',
name,
'--language',
'Python',
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--memory',
'none',
'--idle-timeout',
'300',
'--max-lifetime',
'7200',
'--json',
],
createDir
);

expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(json.projectPath);
const agents = config.agents as Record<string, unknown>[];
expect(agents.length).toBe(1);

const agent = agents[0]!;
const lifecycle = agent.lifecycleConfiguration as Record<string, unknown>;
expect(lifecycle).toBeDefined();
expect(lifecycle.idleRuntimeSessionTimeout).toBe(300);
expect(lifecycle.maxLifetime).toBe(7200);
});

it('creates project with only --idle-timeout', async () => {
const name = `LcIdle${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'create',
'--name',
name,
'--language',
'Python',
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--memory',
'none',
'--idle-timeout',
'600',
'--json',
],
createDir
);

expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(json.projectPath);
const agents = config.agents as Record<string, unknown>[];
const agent = agents[0]!;
const lifecycle = agent.lifecycleConfiguration as Record<string, unknown>;
expect(lifecycle).toBeDefined();
expect(lifecycle.idleRuntimeSessionTimeout).toBe(600);
expect(lifecycle.maxLifetime).toBeUndefined();
});

it('creates project without lifecycle flags — no lifecycleConfiguration in config', async () => {
const name = `LcNone${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'create',
'--name',
name,
'--language',
'Python',
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--memory',
'none',
'--json',
],
createDir
);

expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(json.projectPath);
const agents = config.agents as Record<string, unknown>[];
const agent = agents[0]!;
expect(agent.lifecycleConfiguration).toBeUndefined();
});
});

describe('add agent with lifecycle flags', () => {
it('adds BYO agent with lifecycle config', async () => {
const name = `LcByo${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'add',
'agent',
'--name',
name,
'--type',
'byo',
'--language',
'Python',
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--code-location',
`app/${name}/`,
'--idle-timeout',
'120',
'--max-lifetime',
'3600',
'--json',
],
projectPath
);

expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(projectPath);
const agents = config.agents as Record<string, unknown>[];
const agent = agents.find(a => a.name === name);
expect(agent).toBeDefined();
const lifecycle = agent!.lifecycleConfiguration as Record<string, unknown>;
expect(lifecycle).toBeDefined();
expect(lifecycle.idleRuntimeSessionTimeout).toBe(120);
expect(lifecycle.maxLifetime).toBe(3600);
});

it('adds template agent with only --max-lifetime', async () => {
const name = `LcTmpl${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'add',
'agent',
'--name',
name,
'--framework',
'Strands',
'--model-provider',
'Bedrock',
'--memory',
'none',
'--language',
'Python',
'--max-lifetime',
'14400',
'--json',
],
projectPath
);

expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(projectPath);
const agents = config.agents as Record<string, unknown>[];
const agent = agents.find(a => a.name === name);
expect(agent).toBeDefined();
const lifecycle = agent!.lifecycleConfiguration as Record<string, unknown>;
expect(lifecycle).toBeDefined();
expect(lifecycle.idleRuntimeSessionTimeout).toBeUndefined();
expect(lifecycle.maxLifetime).toBe(14400);
});
});

describe('validation rejects invalid lifecycle values', () => {
it('rejects idle-timeout below 60', async () => {
const name = `LcLow${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'add',
'agent',
'--name',
name,
'--type',
'byo',
'--language',
'Python',
'--code-location',
`app/${name}/`,
'--idle-timeout',
'30',
'--json',
],
projectPath
);

expect(result.exitCode).not.toBe(0);
});

it('rejects max-lifetime above 28800', async () => {
const name = `LcHigh${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'add',
'agent',
'--name',
name,
'--type',
'byo',
'--language',
'Python',
'--code-location',
`app/${name}/`,
'--max-lifetime',
'99999',
'--json',
],
projectPath
);

expect(result.exitCode).not.toBe(0);
});

it('rejects idle-timeout > max-lifetime', async () => {
const name = `LcCross${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'add',
'agent',
'--name',
name,
'--type',
'byo',
'--language',
'Python',
'--code-location',
`app/${name}/`,
'--idle-timeout',
'5000',
'--max-lifetime',
'3000',
'--json',
],
projectPath
);

expect(result.exitCode).not.toBe(0);
});

it('rejects non-integer idle-timeout', async () => {
const name = `LcFloat${Date.now().toString().slice(-6)}`;
const result = await runCLI(
[
'add',
'agent',
'--name',
name,
'--type',
'byo',
'--language',
'Python',
'--code-location',
`app/${name}/`,
'--idle-timeout',
'120.5',
'--json',
],
projectPath
);

expect(result.exitCode).not.toBe(0);
});
});
});
Loading
Loading