use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
use super::ClientBuilder;
use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_JSONRPC};
use crate::error::ClientError;
fn card(interfaces: Vec<AgentInterface>) -> AgentCard {
AgentCard {
url: None,
name: "selection".into(),
version: "1.0".into(),
description: "Interface selection fixture".into(),
supported_interfaces: interfaces,
provider: None,
icon_url: None,
documentation_url: None,
capabilities: AgentCapabilities::none(),
security_schemes: None,
security_requirements: None,
default_input_modes: vec![],
default_output_modes: vec![],
skills: vec![],
signatures: None,
}
}
fn iface(binding: &str, version: &str, url: &str) -> AgentInterface {
AgentInterface {
url: url.into(),
protocol_binding: binding.into(),
protocol_version: version.into(),
tenant: None,
}
}
fn v03_then_v10() -> AgentCard {
card(vec![
iface(BINDING_JSONRPC, "0.3", "http://localhost:1111/v03"),
iface(BINDING_JSONRPC, "1.0", "http://localhost:1111/v1.0"),
])
}
#[test]
fn a_v03_interface_listed_first_is_passed_over_for_v10() {
let builder = ClientBuilder::from_card(&v03_then_v10()).expect("from_card");
assert_eq!(
builder.endpoint, "http://localhost:1111/v1.0",
"this client speaks A2A 1.x; the v0.3 endpoint answers another protocol"
);
}
#[test]
fn version_filtering_applies_before_binding_preference() {
let card = card(vec![
iface(BINDING_GRPC, "0.3.0", "http://localhost:2222/v03"),
iface(BINDING_JSONRPC, "1.0", "http://localhost:1111/v1.0"),
]);
let builder = ClientBuilder::from_card_preferring(&card, &[BINDING_GRPC.into()])
.expect("from_card_preferring");
assert_eq!(
builder.endpoint, "http://localhost:1111/v1.0",
"a preferred binding at an incompatible version is not an option"
);
}
#[test]
fn a_card_with_no_compatible_version_is_refused_with_what_it_offers() {
let card = card(vec![
iface(BINDING_JSONRPC, "0.3", "http://localhost:1111/v03"),
iface(BINDING_HTTP_JSON, "2.0", "http://localhost:1111/v2"),
]);
let Err(ClientError::InvalidEndpoint(msg)) = ClientBuilder::from_card(&card) else {
panic!("a card offering only other majors must be refused");
};
assert!(
msg.contains("JSONRPC 0.3") && msg.contains("HTTP+JSON 2.0"),
"{msg}"
);
}
#[test]
fn prefixed_and_missing_versions_count_as_compatible() {
for version in ["v1.0", "V1", "1", "1.0.0", "1.1-rc1", ""] {
let card = card(vec![
iface(BINDING_JSONRPC, "0.3", "http://localhost:1111/v03"),
iface(BINDING_JSONRPC, version, "http://localhost:1111/ok"),
]);
let builder = ClientBuilder::from_card(&card).expect("from_card");
assert_eq!(builder.endpoint, "http://localhost:1111/ok", "{version:?}");
}
}
#[test]
fn a_lowercase_binding_on_the_card_builds() {
let card = card(vec![iface("jsonrpc", "1.0", "http://localhost:1111")]);
ClientBuilder::from_card(&card)
.expect("from_card")
.build()
.expect("`jsonrpc` is the JSONRPC binding");
let card = card_http_json_lowercase();
ClientBuilder::from_card(&card)
.expect("from_card")
.build()
.expect("`http+json` is the HTTP+JSON binding");
}
fn card_http_json_lowercase() -> AgentCard {
card(vec![iface("http+json", "1.0", "http://localhost:1111")])
}
#[test]
fn an_explicit_binding_in_any_case_builds() {
ClientBuilder::new("http://localhost:1111")
.with_protocol_binding("JsonRpc")
.build()
.expect("binding names are case-insensitive");
}
#[test]
fn an_unbuildable_first_choice_falls_back_to_the_next_interface() {
let card = card(vec![
iface("SOMETHING-NEW", "1.0", "http://localhost:3333"),
iface(BINDING_HTTP_JSON, "1.0", "http://localhost:1111/rest"),
]);
ClientBuilder::from_card_preferring(&card, &[])
.expect("from_card")
.build()
.expect("the HTTP+JSON interface is usable");
}
#[test]
fn sync_build_falls_back_past_grpc() {
let card = card(vec![
iface(BINDING_GRPC, "1.0", "http://localhost:2222"),
iface(BINDING_JSONRPC, "1.0", "http://localhost:1111"),
]);
ClientBuilder::from_card_preferring(&card, &[BINDING_GRPC.into()])
.expect("from_card")
.build()
.expect("JSONRPC is buildable synchronously");
}
#[test]
fn an_explicit_binding_is_not_substituted() {
let card = card(vec![
iface(BINDING_GRPC, "1.0", "http://localhost:2222"),
iface(BINDING_JSONRPC, "1.0", "http://localhost:1111"),
]);
let err = ClientBuilder::from_card(&card)
.expect("from_card")
.with_protocol_binding(BINDING_GRPC)
.build()
.expect_err("GRPC cannot be built synchronously");
assert!(err.to_string().contains("gRPC"), "{err}");
}
#[test]
fn when_nothing_builds_every_attempt_is_reported() {
let card = card(vec![
iface("FOO", "1.0", "http://localhost:3333"),
iface("BAR", "1.0", "http://localhost:4444"),
]);
let err = ClientBuilder::from_card(&card)
.expect("from_card")
.build()
.expect_err("neither binding exists");
let msg = err.to_string();
assert!(msg.contains("FOO") && msg.contains("BAR"), "{msg}");
}
#[test]
fn a_fallback_carries_its_own_tenant() {
let mut rest = iface(BINDING_HTTP_JSON, "1.0", "http://localhost:1111/rest");
rest.tenant = Some("acme".into());
let card = card(vec![iface("FOO", "1.0", "http://localhost:3333"), rest]);
let client = ClientBuilder::from_card_preferring(&card, &[])
.expect("from_card")
.build()
.expect("fallback");
assert_eq!(client.config().tenant.as_deref(), Some("acme"));
}