Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ea63e93
test(e2e): add CLI runner, workspace, and cleanup helpers
Hweinstock Sep 4, 2026
6ea061b
test(e2e): add runtime, harness, and memory suites
Hweinstock Sep 4, 2026
10d9dac
ci: add reusable e2e-test workflow
Hweinstock Sep 4, 2026
c1765ce
test(e2e): run project journeys through compiled CLI
Hweinstock Sep 16, 2026
8e847eb
test(e2e): accept completed empty runtime streams
Hweinstock Sep 16, 2026
acaecfb
test(e2e): reduce suite to runtime lifecycle
Hweinstock Sep 17, 2026
2378641
feat(e2e): add e2e test framework
Hweinstock Sep 17, 2026
466f554
test(e2e): simplify runtime lifecycle suite
Hweinstock Sep 17, 2026
8f66692
ci(e2e): run suite across supported platforms
Hweinstock Sep 17, 2026
c6d2d45
fix(e2e): include all test files
Hweinstock Sep 17, 2026
c70d70e
test(e2e): invoke deployed runtimes concurrently
Hweinstock Sep 17, 2026
a3f1eed
refactor(e2e): clean up test names
Hweinstock Sep 18, 2026
cf72595
refactor(e2e): swap to vitest for native tagging support
Hweinstock Sep 18, 2026
34b6e3d
refactor(logs): add a simple logger
Hweinstock Sep 18, 2026
784d489
docs(e2e): update readme
Hweinstock Sep 18, 2026
f0d6e41
refactor(e2e): keep dev port allocation out of PR
Hweinstock Sep 18, 2026
e60b1ba
ci(e2e): support main pushes and reusable calls
Hweinstock Sep 18, 2026
9be3605
ci(e2e): run reusable suite from CI pushes
Hweinstock Sep 18, 2026
685bd56
fix(test): remove e2e test from unit tests
Hweinstock Sep 18, 2026
427189f
refactor(e2e): rename top level dir to e2eTest
Hweinstock Sep 21, 2026
e02b7be
feat(e2e): add check for no erro on non-http protocols
Hweinstock Sep 21, 2026
8880b26
fix(e2e): shrink some names to avoid hitting the cap
Hweinstock Sep 21, 2026
62c709a
fix(test): ignore e2e tests in unit test suite
Hweinstock Sep 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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

e2e:
if: github.event_name == 'push'

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.

we skip for now for simplicity, but eventually I think we should support a separate tag to run a reduced set.

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.

that is a good idea! when should that suite run and what would it contain?

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.

are you thinking of the idea of a tag that can "mark" certain suites as canary?

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.

are you thinking of the idea of a tag that can "mark" certain suites as canary?

yes exactly!

that is a good idea! when should that suite run and what would it contain?

I have some ideas here.

  • One is that we should run the basic tests across functionality (effectively smoke test each feature)
  • another is that we could maybe check the filepaths touched to guess some tags that should run.
  • another is that a reviewer or harness reviewer could manually/automatically ask for certain tags to be run.

I'll likely start with the first one, and we can explore the others as we get more time. Overall, I want to keep the PR e2e tests short for faster feedback, and reserve all of them for the push event.

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.

2 and 3 would be killer!

uses: ./.github/workflows/e2e-test.yml
permissions:
id-token: write
contents: read
with:
ref: ${{ github.sha }}
secrets: inherit
121 changes: 121 additions & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: e2e-test

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.

separating this out into its own workflow with workflow_call means the canary will be a thin wrapper.

on:
workflow_call:
inputs:
ref:
description: git reference (commit, branch, or tag) to build and test
required: false
type: string
default: ""
tags:
description: Vitest tag expression, for example runtime || canary
required: false
type: string
default: ""
secrets:
WORKFLOW_SECRETS_READER_ROLE_ARN:
required: true
workflow_dispatch:
inputs:
ref:
description: git reference (commit, branch, or tag) to build and test
type: string
default: refactor
tags:
description: Vitest tag expression, for example runtime || canary
type: string
default: ""

concurrency:
group: e2e-test-${{ inputs.ref || github.ref }}-${{ inputs.tags || 'all' }}
cancel-in-progress: false

env:
AGENTCORE_TELEMETRY_DISABLED: "1"

jobs:
authorize:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
outputs:
is-authorized: ${{ steps.check.outputs.is_authorized }}
steps:
- name: Fetch secrets from Secrets Manager
uses: aws/agentcore-devx-devtools/.github/actions/fetch-secrets@31aa3b031a86664e29861d68956e44b07cf21a74
with:
role-arn: ${{ secrets.WORKFLOW_SECRETS_READER_ROLE_ARN }}
repo: AUTHORIZED_USERS
- name: Check authorization
id: authz
uses: aws/agentcore-devx-devtools/.github/actions/check-authorized-user@31aa3b031a86664e29861d68956e44b07cf21a74
with:
subject: ${{ github.actor }}
authorized-users: ${{ env.AUTHORIZED_USERS }}
- name: Determine authorization
id: check
env:
IS_AUTHORIZED: ${{ steps.authz.outputs.is-authorized }}
ACTOR: ${{ github.actor }}
run: |
if [[ "$IS_AUTHORIZED" == "true" ]]; then
echo "Actor ${ACTOR} is authorized"
echo "is_authorized=true" >> "$GITHUB_OUTPUT"
else
echo "Actor ${ACTOR} is not in AUTHORIZED_USERS, skipping"
echo "is_authorized=false" >> "$GITHUB_OUTPUT"
fi

e2e:
name: e2e (${{ matrix.name }})
needs: authorize
if: needs.authorize.outputs.is-authorized == 'true'
runs-on: ${{ fromJSON(matrix.runner) }}
permissions:
id-token: write
contents: read
strategy:
fail-fast: false
matrix:
include:
# Per-job labels stop GitHub from routing a job to the runner CodeBuild created for another job.
# https://docs.aws.amazon.com/codebuild/latest/userguide/sample-github-action-runners-update-labels.html
- name: Linux
runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "e2e-linux"]'
- name: Windows
runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0", "e2e-windows"]'
# CodeBuild does not support macOS.
# https://docs.aws.amazon.com/codebuild/latest/userguide/action-runner-questions.html#action-runner-platform
- name: macOS
runner: '["macos-latest"]'
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: "24"
- uses: astral-sh/setup-uv@v6
- run: bun install --frozen-lockfile
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.E2E_ROLE_ARN }}
aws-region: ${{ vars.E2E_REGION || 'us-east-1' }}
- name: Build CLI
run: bun run build
- name: Run all E2E tests
if: inputs.tags == ''
env:
AGENTCORE_CLI_PATH: node ${{ github.workspace }}/dist/index.js
AWS_REGION: ${{ vars.E2E_REGION || 'us-east-1' }}
run: bun run test:e2e
- name: Run tagged E2E tests
if: inputs.tags != ''
env:
AGENTCORE_CLI_PATH: node ${{ github.workspace }}/dist/index.js
AWS_REGION: ${{ vars.E2E_REGION || 'us-east-1' }}
run: bun run test:e2e -- --tagsFilter="${{ inputs.tags }}"
123 changes: 123 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[test]
preload = ["./src/testing/setup.ts"]
pathIgnorePatterns = ["src/assets/**", "out/**", "dist/**"]
pathIgnorePatterns = ["src/assets/**", "out/**", "dist/**", "e2eTest/**"]
coveragePathIgnorePatterns = ["src/testing/**", "src/assets/**"]
21 changes: 21 additions & 0 deletions e2eTest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# End-to-end tests

The e2e suite deploys and invokes real AgentCore resources. Run it with AWS credentials:

```sh
bun run build
export AGENTCORE_CLI_PATH="node $PWD/dist/index.js"
bun run test:e2e
```

To run tagged tests:

```sh
bun run test:e2e -- --tagsFilter='runtime || canary'
```

Set `AGENTCORE_CLI_PATH` to use a different executable:

```sh
AGENTCORE_CLI_PATH=/path/to/agentcore bun run test:e2e
```
5 changes: 5 additions & 0 deletions e2eTest/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const E2E_PREFIX = "e2e";

export const TAGS = {
RUNTIME: "runtime",
} as const;
22 changes: 22 additions & 0 deletions e2eTest/helpers/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type LogMethod = (...messages: unknown[]) => void;

export type TestLogger = {
debug: LogMethod;
info: LogMethod;
warn: LogMethod;
error: LogMethod;
};

/** Given a scope, creates a thin console-backed logger for E2E diagnostics. */
export function createLogger(scope: string): TestLogger {
const write = (method: (...messages: unknown[]) => void): LogMethod => {
return (...messages) => method(`[${scope}]`, ...messages);
};

return {
debug: write(console.debug),
info: write(console.info),
warn: write(console.warn),
error: write(console.error),
};
}
20 changes: 20 additions & 0 deletions e2eTest/helpers/retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** Given an async operation, retries it until success or the timeout expires. */
export async function retry<T>(
operation: () => Promise<T>,
timeoutMs = 10_000,
intervalMs = 250,
): Promise<T> {
const deadline = Date.now() + timeoutMs;
let lastError: unknown;

while (true) {
try {
return await operation();
} catch (error) {
lastError = error;
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) throw lastError;
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remainingMs)));
}
}
}
76 changes: 76 additions & 0 deletions e2eTest/helpers/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { spawn } from "node:child_process";
import z from "zod";

/** Given a CLI process, captures its standard output, error output, and exit code. */
export type RunResult = {
stdout: string;
stderr: string;
exitCode: number;
};

/** Given an environment variable name, returns its required non-empty value. */
function requireEnv(key: string): string {
const value = process.env[key];
if (!value) throw new Error(`missing environment variable for ${key}`);
return value;
}

/** Given a command argument, returns a shell-safe representation for the current platform. */
function quoteShellArg(value: string): string {
if (process.platform === "win32") {
return `"${value.replaceAll('"', '\\"')}"`;
}
return `'${value.replaceAll("'", "'\\''")}'`;
}

/** Minimal abstraction to handle the running of CLI commands **/
export class CliRunner {
private readonly command = requireEnv("AGENTCORE_CLI_PATH");

/** Given arguments and a working directory, runs the CLI and captures its result. */
run(args: string[], cwd: string): Promise<RunResult> {
return new Promise((resolve, reject) => {
const child = this.start(args, cwd);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => (stdout += chunk));
child.stderr.on("data", (chunk) => (stderr += chunk));
child.on("error", reject);
child.on("close", (exitCode) => resolve({ stdout, stderr, exitCode: exitCode ?? -1 }));
});
}

/** Given arguments and a working directory, starts the CLI and returns its child process. */
start(args: string[], cwd: string) {
const command = [this.command, ...args.map(quoteShellArg)].join(" ");
return spawn(command, {
cwd,
env: { ...process.env, AGENTCORE_TELEMETRY_DISABLED: "1", FORCE_COLOR: "0" },
shell: true,
stdio: ["ignore", "pipe", "pipe"],
});
}
}

/** Given a Zod schema and CLI result, returns typed output or throws a diagnostic error. */
export function parseResult<TSchema extends z.ZodType>(
schema: TSchema,
result: RunResult,
): z.infer<TSchema> {
if (result.exitCode !== 0) {
throw new Error(
`CLI exited ${result.exitCode}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
);
}

const parseResult = schema.safeParse(JSON.parse(result.stdout));

if (!parseResult.success) {
throw new Error(
`CLI output did not match expected. stdout: ${result.stdout}\nstderr: ${result.stderr}\n` +
`error: ${z.prettifyError(parseResult.error)}`,
);
}

return parseResult.data;
}
Loading
Loading