-
Notifications
You must be signed in to change notification settings - Fork 96
feat(e2e): add basic e2e test suite for runtime templates #2339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 6ea061b
test(e2e): add runtime, harness, and memory suites
Hweinstock 10d9dac
ci: add reusable e2e-test workflow
Hweinstock c1765ce
test(e2e): run project journeys through compiled CLI
Hweinstock 8e847eb
test(e2e): accept completed empty runtime streams
Hweinstock acaecfb
test(e2e): reduce suite to runtime lifecycle
Hweinstock 2378641
feat(e2e): add e2e test framework
Hweinstock 466f554
test(e2e): simplify runtime lifecycle suite
Hweinstock 8f66692
ci(e2e): run suite across supported platforms
Hweinstock c6d2d45
fix(e2e): include all test files
Hweinstock c70d70e
test(e2e): invoke deployed runtimes concurrently
Hweinstock a3f1eed
refactor(e2e): clean up test names
Hweinstock cf72595
refactor(e2e): swap to vitest for native tagging support
Hweinstock 34b6e3d
refactor(logs): add a simple logger
Hweinstock 784d489
docs(e2e): update readme
Hweinstock f0d6e41
refactor(e2e): keep dev port allocation out of PR
Hweinstock e60b1ba
ci(e2e): support main pushes and reusable calls
Hweinstock 9be3605
ci(e2e): run reusable suite from CI pushes
Hweinstock 685bd56
fix(test): remove e2e test from unit tests
Hweinstock 427189f
refactor(e2e): rename top level dir to e2eTest
Hweinstock e02b7be
feat(e2e): add check for no erro on non-http protocols
Hweinstock 8880b26
fix(e2e): shrink some names to avoid hitting the cap
Hweinstock 62c709a
fix(test): ignore e2e tests in unit test suite
Hweinstock File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| name: e2e-test | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. separating this out into its own workflow with |
||
| 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 }}" | ||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/**"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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))); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes exactly!
I have some ideas here.
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.
There was a problem hiding this comment.
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!