Skip to content
Closed
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
4 changes: 4 additions & 0 deletions e2eTest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export AGENTCORE_CLI_PATH="node $PWD/dist/index.js"
bun run test:e2e
```

Local `dev` for container templates (for example `agent-python-strands-container`) needs a
container runtime with a reachable daemon (Docker, Podman, or Finch). Without one, their
"runs locally" cases are skipped; deploy and cloud invoke still run.

To run tagged tests:

```sh
Expand Down
18 changes: 18 additions & 0 deletions e2eTest/helpers/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { spawnSync } from "node:child_process";

/** The container tools `agentcore dev` accepts for Container-build runtimes, in its probe order. */
const CONTAINER_TOOLS = ["docker", "podman", "finch"] as const;
const PROBE_TIMEOUT_MS = 15_000;

/**
* Given the current host, returns the first container tool whose daemon is reachable.
* `<tool> info` needs a running daemon, unlike `--version`, which also succeeds on
* hosts that ship only the CLI (for example CodeBuild images without privileged mode).
*/
export function detectContainerTool(): (typeof CONTAINER_TOOLS)[number] | undefined {
for (const tool of CONTAINER_TOOLS) {
const probe = spawnSync(tool, ["info"], { stdio: "ignore", timeout: PROBE_TIMEOUT_MS });
if (!probe.error && probe.status === 0) return tool;
}
return undefined;
}
4 changes: 4 additions & 0 deletions e2eTest/helpers/retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** An error that `retry` rethrows immediately instead of retrying until the deadline. */
export class NonRetryableError extends Error {}

/** Given an async operation, retries it until success or the timeout expires. */
export async function retry<T>(
operation: () => Promise<T>,
Expand All @@ -11,6 +14,7 @@ export async function retry<T>(
try {
return await operation();
} catch (error) {
if (error instanceof NonRetryableError) throw error;
lastError = error;
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) throw lastError;
Expand Down
42 changes: 35 additions & 7 deletions e2eTest/project/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { join } from "node:path";
import z from "zod";
import { E2E_PREFIX, TAGS } from "../constants";
import { CliRunner, parseResult, type RunResult } from "../helpers/run";
import { retry } from "../helpers/retry";
import { detectContainerTool } from "../helpers/container";
import { NonRetryableError, retry } from "../helpers/retry";
import { TIMEOUT_MS } from "../timeouts";

type RuntimeTemplateTestCase = {
name: string;
template: string;
/** Container templates need a container runtime on the host for local `dev`. */
build: "CodeZip" | "Container";
protocol: "HTTP" | "MCP" | "A2A" | "AGUI";
payload: Record<string, unknown>;
invokeFlags?: string[];
Expand All @@ -20,42 +23,49 @@ const RUNTIME_TEMPLATES: RuntimeTemplateTestCase[] = [
{
name: "agent_python_minimal",
template: "agent-python-minimal",
build: "CodeZip",
protocol: "HTTP",
payload: { prompt: "Reply with a short greeting." },
},
{
name: "agent_python_strands",
template: "agent-python-strands",
build: "CodeZip",
protocol: "HTTP",
payload: { prompt: "Reply with a short greeting." },
},
{
name: "py_strands_container",
template: "agent-python-strands-container",
build: "Container",
protocol: "HTTP",
payload: { prompt: "Reply with a short greeting." },
},
{
name: "agent_python_langchain",
template: "agent-python-langchain",
build: "CodeZip",
protocol: "HTTP",
payload: { prompt: "Reply with a short greeting." },
},
{
name: "agent_ts_strands",
template: "agent-typescript-strands",
build: "CodeZip",
protocol: "HTTP",
payload: { prompt: "Reply with a short greeting." },
},
{
name: "agent_ts_vercel",
template: "agent-typescript-vercel",
build: "CodeZip",
protocol: "HTTP",
payload: { prompt: "Reply with a short greeting." },
},
{
name: "mcp_python_fastmcp",
template: "mcp-python-fastmcp",
build: "CodeZip",
protocol: "MCP",
payload: {
jsonrpc: "2.0",
Expand All @@ -79,6 +89,7 @@ const RUNTIME_TEMPLATES: RuntimeTemplateTestCase[] = [
{
name: "a2a_python_strands",
template: "a2a-python-strands",
build: "CodeZip",
protocol: "A2A",
payload: {
jsonrpc: "2.0",
Expand All @@ -96,6 +107,7 @@ const RUNTIME_TEMPLATES: RuntimeTemplateTestCase[] = [
{
name: "agui_python_strands",
template: "agui-python-strands",
build: "CodeZip",
protocol: "AGUI",
payload: {
threadId: "agentcore-e2e",
Expand Down Expand Up @@ -177,22 +189,27 @@ describe(

describe("local invocation", { sequential: true }, () => {
const runtimePorts = new Map<string, number>();
const runtimeFailures = new Map<string, string>();
const containerTool = detectContainerTool();
let dev: ReturnType<CliRunner["start"]> | undefined;
let pendingOutput = "";
let devOutput = "";

/** Given dev-process output, records the ports announced by running runtimes. */
/** Given dev-process output, records announced ports and startup failures per runtime. */
const captureDevOutput = (chunk: Buffer) => {
const text = chunk.toString();
devOutput += text;
pendingOutput += text;
const lines = pendingOutput.split(/\r?\n/);
pendingOutput = lines.pop() ?? "";

// parse the out for the ports each agent is running on
// parse the output for the ports each agent is running on, and for agents
// the supervisor gave up on (dev keeps serving the others, so it never exits)
for (const line of lines) {
const match = line.match(/Agent '([^']+)' is running on port (\d+)\./);
if (match?.[1] && match[2]) runtimePorts.set(match[1], Number(match[2]));
const running = line.match(/Agent '([^']+)' is running on port (\d+)\./);
if (running?.[1] && running[2]) runtimePorts.set(running[1], Number(running[2]));
const failed = line.match(/Agent '([^']+)' failed to start: (.*)$/);
if (failed?.[1] && failed[2]) runtimeFailures.set(failed[1], failed[2]);
}
};

Expand All @@ -210,10 +227,14 @@ describe(
await new Promise<void>((resolve) => dev?.once("close", resolve));
});

test.each(RUNTIME_TEMPLATES)(
test.for(RUNTIME_TEMPLATES)(
"$name runs locally",
{ concurrent: true, timeout: TIMEOUT_MS.PROJECT_INVOKE },
async (runtime) => {
async (runtime, { skip }) => {
skip(
runtime.build === "Container" && !containerTool,
"no container runtime (docker, podman, or finch) is available on this host",
);
const sessionId = getSessionId(`${runtime.name}`);

// The server may take a bit to get ready, so we retry on a timeout.
Expand All @@ -227,6 +248,13 @@ describe(
);
}

const failure = runtimeFailures.get(runtime.name);
if (failure) {
throw new NonRetryableError(
`Runtime '${runtime.name}' failed to start: ${failure}\nstdout/stderr = ${devOutput}`,
);
}

const port = runtimePorts.get(runtime.name);
if (!port) {
throw new Error(
Expand Down
Loading