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
8 changes: 8 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ jobs:
ANTHROPIC_API_KEY: ${{ env.E2E_ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
E2E_EFS_ACCESS_POINT_ARN: ${{ env.E2E_EFS_ACCESS_POINT_ARN }}
E2E_S3_ACCESS_POINT_ARN: ${{ env.E2E_S3_ACCESS_POINT_ARN }}
E2E_FILESYSTEM_SUBNET_ID: ${{ env.E2E_FILESYSTEM_SUBNET_ID }}
E2E_FILESYSTEM_SECURITY_GROUP_ID: ${{ env.E2E_FILESYSTEM_SECURITY_GROUP_ID }}
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.ga_extra }}

- name: Install preview CLI globally
Expand All @@ -161,5 +165,9 @@ jobs:
ANTHROPIC_API_KEY: ${{ env.E2E_ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
E2E_EFS_ACCESS_POINT_ARN: ${{ env.E2E_EFS_ACCESS_POINT_ARN }}
E2E_S3_ACCESS_POINT_ARN: ${{ env.E2E_S3_ACCESS_POINT_ARN }}
E2E_FILESYSTEM_SUBNET_ID: ${{ env.E2E_FILESYSTEM_SUBNET_ID }}
E2E_FILESYSTEM_SECURITY_GROUP_ID: ${{ env.E2E_FILESYSTEM_SECURITY_GROUP_ID }}
BUILD_PREVIEW: '1'
run: npx vitest run --project e2e e2e-tests/harness-bedrock.test.ts ${{ steps.changed.outputs.harness_extra }}
50 changes: 44 additions & 6 deletions e2e-tests/e2e-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const baseCanRun = prereqs.npm && prereqs.git && prereqs.uv && hasAws;
interface E2EConfig {
framework: string;
modelProvider: string;
requiredEnvVar?: string;
/** Env var holding the API key — must be set for the suite to run, and its value is passed as --api-key. */
apiKeyEnvVar?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we seperate out the apiKey into their own variable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requiredEnvVar was doing two different things: (1) gating the test suite, and (2) forwarding the env var's value as --api-key to the create command. These had to be separated because the filesystem test needs to gate on 4 env vars (E2E_EFS_ACCESS_POINT_ARN, E2E_S3_ACCESS_POINT_ARN, E2E_FILESYSTEM_SUBNET_ID, E2E_FILESYSTEM_SECURITY_GROUP_ID) that are not API keys — they're ARNs/IDs used in the --efs-access-point-arn etc. args. requiredEnvVars: string[] handles "gate only" for any number of vars; apiKeyEnvVar handles the "gate + pass as --api-key" case that existing tests need.

/** Additional env vars that must all be set for the suite to run. */
requiredEnvVars?: string[];
build?: string;
memory?: string;
/** Language for the agent project. Defaults to 'Python'. */
Expand All @@ -38,12 +41,27 @@ interface E2EConfig {
idleTimeout?: number;
maxLifetime?: number;
};
/** Custom prompt for the invoke test. Defaults to 'Say hello'. */
invokePrompt?: string;
/** Optional assertion on the invoke response string. */
invokeResponseCheck?: (response: string) => void;
/** EFS access point mounts. Requires VPC network mode. */
efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
/** S3 Files access point mounts. Requires VPC network mode. */
s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
/** VPC network mode config. Required when using filesystem mounts. */
networkConfig?: {
networkMode: 'VPC';
subnets: string;
securityGroups: string;
};
}

export function createE2ESuite(cfg: E2EConfig) {
const hasApiKey = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const hasRequiredEnvVars =
(!cfg.apiKeyEnvVar || !!process.env[cfg.apiKeyEnvVar]) && (cfg.requiredEnvVars ?? []).every(v => !!process.env[v]);
const needsUv = cfg.language !== 'TypeScript';
const canRun = prereqs.npm && prereqs.git && hasAws && hasApiKey && (!needsUv || prereqs.uv);
const canRun = prereqs.npm && prereqs.git && hasAws && hasRequiredEnvVars && (!needsUv || prereqs.uv);

describe.sequential(`e2e: ${cfg.framework}/${cfg.modelProvider} — create → deploy → invoke`, () => {
let testDir: string;
Expand Down Expand Up @@ -86,11 +104,27 @@ export function createE2ESuite(cfg: E2EConfig) {
}

// Pass API key so the credential is registered in the project and .env.local
const apiKey = cfg.requiredEnvVar ? process.env[cfg.requiredEnvVar] : undefined;
const apiKey = cfg.apiKeyEnvVar ? process.env[cfg.apiKeyEnvVar] : undefined;
if (apiKey) {
createArgs.push('--api-key', apiKey);
}

if (cfg.networkConfig) {
createArgs.push('--network-mode', cfg.networkConfig.networkMode);
createArgs.push('--subnets', cfg.networkConfig.subnets);
createArgs.push('--security-groups', cfg.networkConfig.securityGroups);
}

for (const ap of cfg.efsAccessPoints ?? []) {
createArgs.push('--efs-access-point-arn', ap.accessPointArn);
createArgs.push('--efs-mount-path', ap.mountPath);
}

for (const ap of cfg.s3AccessPoints ?? []) {
createArgs.push('--s3-access-point-arn', ap.accessPointArn);
createArgs.push('--s3-mount-path', ap.mountPath);
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
Expand Down Expand Up @@ -148,7 +182,7 @@ export function createE2ESuite(cfg: E2EConfig) {
await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--prompt', 'Say hello', '--runtime', agentName, '--json'],
['invoke', '--prompt', cfg.invokePrompt ?? 'Say hello', '--runtime', agentName, '--json'],
projectPath
);

Expand All @@ -159,8 +193,12 @@ export function createE2ESuite(cfg: E2EConfig) {

expect(result.exitCode, `Invoke failed: ${result.stderr}`).toBe(0);

const json = parseJsonOutput(result.stdout) as { success: boolean };
const json = parseJsonOutput(result.stdout) as { success: boolean; response?: string };
expect(json.success, 'Invoke should report success').toBe(true);
if (cfg.invokeResponseCheck) {
expect(json.response, 'Invoke should return a non-empty response').toBeTruthy();
cfg.invokeResponseCheck(json.response!);
}
},
3,
15000
Expand Down
Loading
Loading