use std::time::Duration;
use a2a_agents::{
AgentRuntime, AgentSpec, ContainerHardening, ContainerRuntime, EnvAllowlist, Recovered,
RuntimeHealth,
};
const IMAGE: &str = "a2a-agents:latest";
fn docker_available() -> bool {
std::process::Command::new("docker")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn image_available(image: &str) -> bool {
std::process::Command::new("docker")
.args(["image", "inspect", image])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn every_feature_the_a2a_binary_requires_is_a_default() {
let manifest = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"))
.expect("read Cargo.toml");
fn feature_list<'a>(manifest: &'a str, key: &str) -> Vec<&'a str> {
let prefix = format!("{key} = [");
manifest
.lines()
.find_map(|line| line.trim().strip_prefix(&prefix))
.unwrap_or_else(|| panic!("no `{key}` list in the manifest"))
.trim_end_matches(']')
.split(',')
.map(|feature| feature.trim().trim_matches('"'))
.filter(|feature| !feature.is_empty())
.collect()
}
let bin_section = manifest
.split("[[bin]]")
.find(|section| section.lines().any(|line| line.trim() == r#"name = "a2a""#))
.expect("the a2a bin section");
let required = feature_list(bin_section, "required-features");
let default = feature_list(&manifest, "default");
assert!(!required.is_empty(), "parsed no required features");
for feature in &required {
assert!(
default.contains(feature),
"the `a2a` binary requires `{feature}`, which is not a default feature — \
`cargo install a2a-agents` will silently skip the binary, and `docker build` \
will fail with \"target `a2a` requires the features\". Defaults: {default:?}"
);
}
}
#[test]
fn the_dockerfile_builds_the_binary_with_default_features() {
let dockerfile = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/Dockerfile"))
.expect("read Dockerfile");
let pinned: Vec<&str> = dockerfile
.lines()
.map(str::trim)
.filter(|line| line.starts_with("--features") || line.contains(" --features "))
.collect();
assert!(
pinned.is_empty(),
"the Dockerfile pins features instead of relying on the defaults, so the image \
can drift from what `cargo install` produces: {pinned:?}"
);
}
#[tokio::test]
async fn recover_query_is_accepted_by_a_real_engine() {
if !docker_available() {
eprintln!("skipping recover_query_is_accepted_by_a_real_engine: docker not available");
return;
}
match ContainerRuntime::new().recover().await {
Ok(Recovered::Adopted(_)) => {}
other => panic!("the engine rejected the recovery query: {other:?}"),
}
}
#[tokio::test]
async fn container_runtime_full_lifecycle() {
if !docker_available() {
eprintln!("skipping container_runtime_full_lifecycle: docker not available");
return;
}
if !image_available(IMAGE) {
eprintln!("skipping container_runtime_full_lifecycle: image '{IMAGE}' not built");
return;
}
let port = std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
unsafe {
std::env::set_var("A2A_CONTAINER_TEST_SECRET", "injected-from-host");
}
let config_path = std::env::temp_dir().join(format!("container_test_{port}.toml"));
std::fs::write(
&config_path,
format!(
r#"
[agent]
name = "Container Test Agent"
description = "${{A2A_CONTAINER_TEST_SECRET}}"
[handler]
type = "echo"
[server]
http_port = {port}
"#
),
)
.unwrap();
let rt =
ContainerRuntime::new().with_allowed_env(EnvAllowlist::new(["A2A_CONTAINER_TEST_SECRET"]));
let spec = AgentSpec::from_config_path(&config_path).expect("spec from config");
let id = rt.provision(spec).await.expect("provision");
assert_eq!(
rt.health(&id).await.unwrap(),
RuntimeHealth::Provisioned,
"a created-but-unstarted container is Provisioned"
);
rt.start(&id).await.expect("start");
let mut health = RuntimeHealth::Provisioned;
for _ in 0..60 {
health = rt.health(&id).await.unwrap();
if health == RuntimeHealth::Healthy {
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
assert_eq!(
health,
RuntimeHealth::Healthy,
"agent should become Healthy"
);
let card = a2a_rs::fetch_agent_card(&format!("http://127.0.0.1:{port}"))
.await
.expect("fetch agent card");
assert_eq!(
card.description, "injected-from-host",
"container should expand env refs from injected pass-through vars"
);
let restarted = ContainerRuntime::new();
let Recovered::Adopted(adopted) = restarted.recover().await.expect("recover") else {
panic!("the container runtime is durable and must report Adopted");
};
assert!(
adopted.contains(&id),
"recovery must find the running container: adopted {adopted:?}"
);
assert_eq!(
restarted.health(&id).await.unwrap(),
RuntimeHealth::Healthy,
"a recovered agent must be health-checkable, port and all"
);
assert!(
restarted
.list()
.await
.unwrap()
.iter()
.any(|s| s.id == id && s.endpoint == format!("http://127.0.0.1:{port}")),
"the published port must be recovered too, or the endpoint is wrong"
);
restarted.stop(&id).await.expect("stop after recovery");
assert_eq!(rt.health(&id).await.unwrap(), RuntimeHealth::Stopped);
let stopped = ContainerRuntime::new();
assert!(
stopped.recover().await.unwrap().adopted().contains(&id),
"recovery must adopt stopped containers too, or they become unmanageable"
);
let _ = std::process::Command::new("docker")
.args(["rm", "-f", &format!("a2a-agent-{id}")])
.output();
let _ = std::fs::remove_file(&config_path);
}
fn inspect(container: &str, template: &str) -> String {
let out = std::process::Command::new("docker")
.args(["inspect", "-f", template, container])
.output()
.expect("run docker inspect");
assert!(
out.status.success(),
"docker inspect {template} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[tokio::test]
async fn a_hardened_agent_still_serves_and_the_engine_applied_the_flags() {
if !docker_available() {
eprintln!("skipping a_hardened_agent_still_serves: docker not available");
return;
}
if !image_available(IMAGE) {
eprintln!("skipping a_hardened_agent_still_serves: image '{IMAGE}' not built");
return;
}
let port = std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let config_path = std::env::temp_dir().join(format!("container_hardened_{port}.toml"));
std::fs::write(
&config_path,
format!(
r#"
[agent]
name = "Hardened Agent {port}"
[handler]
type = "echo"
[server]
http_port = {port}
[server.storage]
type = "inmemory"
"#
),
)
.unwrap();
let rt = ContainerRuntime::new().with_hardening(
ContainerHardening::default()
.with_memory("256m")
.with_cpus("1"),
);
let spec = AgentSpec::from_config_path(&config_path).expect("spec from config");
let id = rt.provision(spec).await.expect("provision");
rt.start(&id).await.expect("start");
let mut health = RuntimeHealth::Provisioned;
for _ in 0..60 {
health = rt.health(&id).await.unwrap();
if health == RuntimeHealth::Healthy {
break;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
let container = format!("a2a-agent-{id}");
assert_eq!(
health,
RuntimeHealth::Healthy,
"a fully hardened agent must still serve its card; container logs:\n{}",
String::from_utf8_lossy(
&std::process::Command::new("docker")
.args(["logs", &container])
.output()
.map(|o| [o.stdout, o.stderr].concat())
.unwrap_or_default()
)
);
assert_eq!(
inspect(&container, "{{.HostConfig.ReadonlyRootfs}}"),
"true"
);
assert_eq!(inspect(&container, "{{.HostConfig.CapDrop}}"), "[ALL]");
assert_eq!(inspect(&container, "{{.HostConfig.PidsLimit}}"), "512");
assert!(
inspect(&container, "{{.HostConfig.SecurityOpt}}").contains("no-new-privileges"),
"no-new-privileges must reach the engine"
);
assert_eq!(
inspect(&container, "{{.HostConfig.Memory}}"),
(256 * 1024 * 1024).to_string()
);
let user = inspect(&container, "{{.Config.User}}");
assert!(
user != "0" && user != "root" && !user.is_empty(),
"the agent image must not run as root, got {user:?}"
);
rt.stop(&id).await.expect("stop");
let _ = std::process::Command::new("docker")
.args(["rm", "-f", &container])
.output();
let _ = std::fs::remove_file(&config_path);
}