1mod transport_factory;
35
36use std::time::Duration;
37
38use a2a_protocol_types::{AgentCard, AgentInterface};
39
40use crate::config::{ClientConfig, TlsConfig};
41use crate::error::{ClientError, ClientResult};
42use crate::interceptor::{CallInterceptor, InterceptorChain};
43use crate::retry::RetryPolicy;
44use crate::transport::Transport;
45
46#[allow(dead_code)]
54pub(crate) const SUPPORTED_PROTOCOL_MAJOR: u32 = 1;
55
56#[allow(dead_code)] pub(crate) fn protocol_version_mismatch(protocol_version: &str) -> Option<&str> {
72 if protocol_version.is_empty() {
73 return None;
74 }
75 let major = protocol_version
76 .split('.')
77 .next()
78 .and_then(|s| s.parse::<u32>().ok());
79 if major == Some(SUPPORTED_PROTOCOL_MAJOR) {
80 None
81 } else {
82 Some(protocol_version)
83 }
84}
85
86pub struct ClientBuilder {
93 pub(super) endpoint: String,
94 pub(super) transport_override: Option<Box<dyn Transport>>,
95 pub(super) interceptors: InterceptorChain,
96 pub(super) config: ClientConfig,
97 pub(super) preferred_binding: Option<String>,
98 pub(super) retry_policy: Option<RetryPolicy>,
99 pub(super) peer_honours_idempotency: bool,
106 pub(super) card_interfaces: Vec<AgentInterface>,
113 pub(super) grpc_bare_address_scheme: crate::config::GrpcBareAddressScheme,
122 #[cfg(feature = "grpc-tls")]
127 pub(super) grpc_tls_config: Option<crate::transport::grpc::ClientTlsConfig>,
128}
129
130fn select_interface<'a>(card: &'a AgentCard, preferences: &[String]) -> Option<&'a AgentInterface> {
139 for wanted in preferences {
140 if let Some(iface) = card
141 .supported_interfaces
142 .iter()
143 .find(|i| i.protocol_binding.eq_ignore_ascii_case(wanted))
144 {
145 return Some(iface);
146 }
147 }
148 card.supported_interfaces.first()
149}
150
151impl ClientBuilder {
152 #[must_use]
157 pub fn new(endpoint: impl Into<String>) -> Self {
158 Self {
159 endpoint: endpoint.into(),
160 transport_override: None,
161 interceptors: InterceptorChain::new(),
162 config: ClientConfig::default(),
163 preferred_binding: None,
164 retry_policy: None,
165 peer_honours_idempotency: false,
166 grpc_bare_address_scheme: crate::config::GrpcBareAddressScheme::default(),
167 #[cfg(feature = "grpc-tls")]
168 grpc_tls_config: None,
169 card_interfaces: Vec::new(),
170 }
171 }
172
173 pub fn from_card(card: &AgentCard) -> ClientResult<Self> {
180 Self::from_card_preferring(card, &ClientConfig::default().preferred_bindings)
181 }
182
183 pub fn from_card_preferring(card: &AgentCard, preferences: &[String]) -> ClientResult<Self> {
210 let first = select_interface(card, preferences).ok_or_else(|| {
211 ClientError::InvalidEndpoint("agent card has no supported interfaces".into())
212 })?;
213 let (endpoint, binding) = (first.url.clone(), first.protocol_binding.clone());
214
215 #[cfg(feature = "tracing")]
217 if let Some(mismatched) = protocol_version_mismatch(&first.protocol_version) {
218 trace_warn!(
219 agent = %card.name,
220 protocol_version = %mismatched,
221 supported_major = SUPPORTED_PROTOCOL_MAJOR,
222 "agent protocol version may be incompatible with this client"
223 );
224 }
225
226 let peer_honours_idempotency = card.capabilities.extensions.as_ref().is_some_and(|exts| {
232 exts.iter()
233 .any(|e| e.uri == a2a_protocol_types::idempotency::IDEMPOTENCY_EXTENSION_URI)
234 });
235
236 Ok(Self {
237 endpoint,
238 transport_override: None,
239 interceptors: InterceptorChain::new(),
240 peer_honours_idempotency,
241 config: ClientConfig {
242 tenant: first.tenant.clone(),
244 preferred_bindings: preferences.to_vec(),
249 ..ClientConfig::default()
250 },
251 preferred_binding: Some(binding),
252 retry_policy: None,
253 grpc_bare_address_scheme: crate::config::GrpcBareAddressScheme::default(),
254 #[cfg(feature = "grpc-tls")]
255 grpc_tls_config: None,
256 card_interfaces: card.supported_interfaces.clone(),
257 })
258 }
259
260 #[must_use]
264 pub const fn with_timeout(mut self, timeout: Duration) -> Self {
265 self.config.request_timeout = timeout;
266 self
267 }
268
269 #[must_use]
274 pub const fn with_stream_connect_timeout(mut self, timeout: Duration) -> Self {
275 self.config.stream_connect_timeout = timeout;
276 self
277 }
278
279 #[must_use]
284 pub const fn with_connection_timeout(mut self, timeout: Duration) -> Self {
285 self.config.connection_timeout = timeout;
286 self
287 }
288
289 #[must_use]
295 pub const fn with_max_response_size(mut self, max_bytes: usize) -> Self {
296 self.config.max_response_size = max_bytes;
297 self
298 }
299
300 #[must_use]
318 pub fn chosen_interface(&self) -> Option<&AgentInterface> {
319 let binding = self.preferred_binding.as_deref()?;
320 self.card_interfaces
321 .iter()
322 .find(|i| i.protocol_binding.eq_ignore_ascii_case(binding) && i.url == self.endpoint)
323 }
324
325 #[must_use]
342 pub fn with_protocol_binding(mut self, binding: impl Into<String>) -> Self {
343 let binding = binding.into();
344 let resolved = self
345 .card_interfaces
346 .iter()
347 .find(|i| i.protocol_binding.eq_ignore_ascii_case(&binding))
348 .map(|i| (i.url.clone(), i.tenant.clone()));
349 if let Some((url, tenant)) = resolved {
350 self.endpoint = url;
351 self.config.tenant = tenant;
352 }
353 self.preferred_binding = Some(binding);
354 self
355 }
356
357 #[must_use]
369 pub const fn with_grpc_bare_address_scheme(
370 mut self,
371 scheme: crate::config::GrpcBareAddressScheme,
372 ) -> Self {
373 self.grpc_bare_address_scheme = scheme;
374 self
375 }
376
377 #[cfg(feature = "grpc-tls")]
384 #[must_use]
385 pub fn with_grpc_tls_config(mut self, tls: crate::transport::grpc::ClientTlsConfig) -> Self {
386 self.grpc_tls_config = Some(tls);
387 self
388 }
389
390 #[must_use]
392 pub fn with_accepted_output_modes(mut self, modes: Vec<String>) -> Self {
393 self.config.accepted_output_modes = modes;
394 self
395 }
396
397 #[must_use]
399 pub const fn with_history_length(mut self, length: u32) -> Self {
400 self.config.history_length = Some(length);
401 self
402 }
403
404 #[must_use]
410 pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
411 self.config.tenant = Some(tenant.into());
412 self
413 }
414
415 #[must_use]
417 pub const fn with_return_immediately(mut self, val: bool) -> Self {
418 self.config.return_immediately = val;
419 self
420 }
421
422 #[must_use]
427 pub fn with_custom_transport(mut self, transport: impl Transport) -> Self {
428 self.transport_override = Some(Box::new(transport));
429 self
430 }
431
432 #[must_use]
434 pub const fn without_tls(mut self) -> Self {
435 self.config.tls = TlsConfig::Disabled;
436 self
437 }
438
439 #[must_use]
458 pub const fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
459 self.retry_policy = Some(policy);
460 self
461 }
462
463 #[must_use]
482 pub const fn with_peer_honouring_idempotency(mut self, honours: bool) -> Self {
483 self.peer_honours_idempotency = honours;
484 self
485 }
486
487 #[must_use]
491 pub fn with_interceptor<I: CallInterceptor>(mut self, interceptor: I) -> Self {
492 self.interceptors.push(interceptor);
493 self
494 }
495}
496
497impl std::fmt::Debug for ClientBuilder {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 f.debug_struct("ClientBuilder")
500 .field("endpoint", &self.endpoint)
501 .field("preferred_binding", &self.preferred_binding)
502 .finish_non_exhaustive()
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::config::{BINDING_GRPC, BINDING_HTTP_JSON, BINDING_JSONRPC, BINDING_REST};
510 use std::time::Duration;
511
512 fn card_with(interfaces: Vec<a2a_protocol_types::AgentInterface>) -> AgentCard {
524 use a2a_protocol_types::AgentCapabilities;
525
526 AgentCard {
527 url: None,
528 name: "prefs".into(),
529 version: "1.0".into(),
530 description: "Binding preference fixture".into(),
531 supported_interfaces: interfaces,
532 provider: None,
533 icon_url: None,
534 documentation_url: None,
535 capabilities: AgentCapabilities::none(),
536 security_schemes: None,
537 security_requirements: None,
538 default_input_modes: vec![],
539 default_output_modes: vec![],
540 skills: vec![],
541 signatures: None,
542 }
543 }
544
545 fn iface(binding: &str, url: &str) -> a2a_protocol_types::AgentInterface {
546 a2a_protocol_types::AgentInterface {
547 url: url.into(),
548 protocol_binding: binding.into(),
549 protocol_version: "1.0.0".into(),
550 tenant: None,
551 }
552 }
553
554 fn jsonrpc_then_grpc() -> AgentCard {
556 card_with(vec![
557 iface(BINDING_JSONRPC, "http://localhost:1111"),
558 iface(BINDING_GRPC, "http://localhost:2222"),
559 ])
560 }
561
562 #[test]
563 fn from_card_prefers_the_callers_binding_order_over_the_cards() {
564 let builder =
565 ClientBuilder::from_card_preferring(&jsonrpc_then_grpc(), &[BINDING_GRPC.into()])
566 .expect("from_card_preferring");
567
568 assert_eq!(
569 builder.endpoint, "http://localhost:2222",
570 "the caller ranked GRPC; the card's first interface is JSONRPC. \
571 Taking the card's order would give :1111"
572 );
573 assert_eq!(builder.preferred_binding.as_deref(), Some(BINDING_GRPC));
574 }
575
576 #[test]
577 fn a_later_preference_wins_when_the_earlier_one_is_not_offered() {
578 let builder = ClientBuilder::from_card_preferring(
579 &jsonrpc_then_grpc(),
580 &[BINDING_HTTP_JSON.into(), BINDING_GRPC.into()],
581 )
582 .expect("from_card_preferring");
583
584 assert_eq!(
585 builder.endpoint, "http://localhost:2222",
586 "HTTP+JSON is unavailable, so the second preference (GRPC) applies"
587 );
588 }
589
590 #[test]
591 fn an_unmatched_preference_falls_back_to_the_cards_first_interface() {
592 let builder =
593 ClientBuilder::from_card_preferring(&jsonrpc_then_grpc(), &[BINDING_HTTP_JSON.into()])
594 .expect("from_card_preferring");
595
596 assert_eq!(
597 builder.endpoint, "http://localhost:1111",
598 "no ranked binding is offered, so the card's own first choice is used"
599 );
600 assert_eq!(builder.preferred_binding.as_deref(), Some(BINDING_JSONRPC));
601 }
602
603 #[test]
604 fn an_empty_preference_list_takes_the_cards_first_interface() {
605 let builder = ClientBuilder::from_card_preferring(&jsonrpc_then_grpc(), &[])
606 .expect("from_card_preferring");
607
608 assert_eq!(builder.endpoint, "http://localhost:1111");
609 }
610
611 #[test]
617 fn chosen_interface_is_the_one_from_card_picked() {
618 let builder = ClientBuilder::from_card(&jsonrpc_then_grpc()).expect("from_card");
619 let chosen = builder.chosen_interface().expect("came from a card");
620 assert_eq!(chosen.protocol_binding, BINDING_JSONRPC);
621 assert_eq!(chosen.url, "http://localhost:1111");
622
623 let moved = builder.with_protocol_binding(BINDING_GRPC);
624 let chosen = moved.chosen_interface().expect("GRPC is on the card");
625 assert_eq!(chosen.url, "http://localhost:2222", "follows the endpoint");
626
627 assert!(
628 moved
629 .with_protocol_binding(BINDING_HTTP_JSON)
630 .chosen_interface()
631 .is_none(),
632 "a binding the card does not advertise leaves no card interface to report"
633 );
634 assert!(
635 ClientBuilder::new("http://localhost:3333")
636 .chosen_interface()
637 .is_none(),
638 "no card, no interface"
639 );
640 }
641
642 #[test]
643 fn binding_preference_matches_case_insensitively() {
644 let builder =
648 ClientBuilder::from_card_preferring(&jsonrpc_then_grpc(), &["gRpC".to_owned()])
649 .expect("from_card_preferring");
650
651 assert_eq!(builder.endpoint, "http://localhost:2222");
652 }
653
654 #[test]
655 fn from_card_applies_the_default_preference_list() {
656 let card = card_with(vec![
659 iface(BINDING_GRPC, "http://localhost:2222"),
660 iface(BINDING_JSONRPC, "http://localhost:1111"),
661 ]);
662
663 let builder = ClientBuilder::from_card(&card).expect("from_card");
664
665 assert_eq!(
666 builder.endpoint, "http://localhost:1111",
667 "from_card must honour ClientConfig's default preference (JSONRPC), \
668 not the card's own first entry (GRPC)"
669 );
670 assert_eq!(builder.preferred_binding.as_deref(), Some(BINDING_JSONRPC));
671 }
672
673 #[test]
674 fn a_single_interface_card_is_used_whatever_the_preference() {
675 let card = card_with(vec![iface(BINDING_JSONRPC, "http://localhost:1111")]);
676
677 let builder = ClientBuilder::from_card_preferring(&card, &[BINDING_GRPC.into()])
678 .expect("from_card_preferring");
679
680 assert_eq!(
681 builder.endpoint, "http://localhost:1111",
682 "an agent that speaks nothing the caller ranked is still worth \
683 talking to; refusing to connect would be a worse answer"
684 );
685 }
686
687 #[test]
688 fn the_applied_preference_is_recorded_in_the_built_config() {
689 let prefs = vec![BINDING_GRPC.to_owned(), BINDING_JSONRPC.to_owned()];
692 let builder = ClientBuilder::from_card_preferring(&jsonrpc_then_grpc(), &prefs)
693 .expect("from_card_preferring");
694
695 assert_eq!(builder.config.preferred_bindings, prefs);
696 }
697
698 #[test]
707 fn switching_binding_on_a_card_builder_moves_the_endpoint_too() {
708 let builder = ClientBuilder::from_card(&jsonrpc_then_grpc())
709 .expect("from_card")
710 .with_protocol_binding(BINDING_GRPC);
711
712 assert_eq!(
713 builder.endpoint, "http://localhost:2222",
714 "the card advertises GRPC at :2222; keeping :1111 would speak gRPC \
715 to the JSON-RPC port"
716 );
717 assert_eq!(builder.preferred_binding.as_deref(), Some(BINDING_GRPC));
718 }
719
720 #[test]
721 fn switching_binding_carries_that_interfaces_tenant() {
722 let card = card_with(vec![
723 iface(BINDING_JSONRPC, "http://localhost:1111"),
724 a2a_protocol_types::AgentInterface {
725 tenant: Some("grpc-tenant".into()),
726 ..iface(BINDING_GRPC, "http://localhost:2222")
727 },
728 ]);
729
730 let builder = ClientBuilder::from_card(&card)
731 .expect("from_card")
732 .with_protocol_binding(BINDING_GRPC);
733
734 assert_eq!(
735 builder.config.tenant.as_deref(),
736 Some("grpc-tenant"),
737 "tenant is per-interface; the old interface's tenant does not \
738 survive a move to a different one"
739 );
740 }
741
742 #[test]
743 fn with_tenant_after_a_binding_switch_wins() {
744 let builder = ClientBuilder::from_card(&jsonrpc_then_grpc())
745 .expect("from_card")
746 .with_protocol_binding(BINDING_GRPC)
747 .with_tenant("explicit");
748
749 assert_eq!(builder.config.tenant.as_deref(), Some("explicit"));
750 }
751
752 #[test]
753 fn switching_to_a_binding_the_card_lacks_leaves_the_endpoint_alone() {
754 let builder = ClientBuilder::from_card(&jsonrpc_then_grpc())
755 .expect("from_card")
756 .with_protocol_binding(BINDING_HTTP_JSON);
757
758 assert_eq!(
759 builder.endpoint, "http://localhost:1111",
760 "nothing to resolve against, so the caller's endpoint stands"
761 );
762 assert_eq!(
763 builder.preferred_binding.as_deref(),
764 Some(BINDING_HTTP_JSON)
765 );
766 }
767
768 #[test]
769 fn a_plain_new_builder_keeps_its_endpoint_across_a_binding_switch() {
770 let builder =
773 ClientBuilder::new("http://localhost:8080").with_protocol_binding(BINDING_REST);
774
775 assert_eq!(builder.endpoint, "http://localhost:8080");
776 assert_eq!(builder.preferred_binding.as_deref(), Some(BINDING_REST));
777 }
778
779 #[test]
780 fn from_card_preferring_rejects_a_card_with_no_interfaces() {
781 let result =
782 ClientBuilder::from_card_preferring(&card_with(vec![]), &[BINDING_GRPC.into()]);
783 assert!(result.is_err(), "empty interfaces should return error");
784 }
785
786 fn card_advertising(extension_uris: &[&str]) -> AgentCard {
802 let mut card = card_with(vec![iface(BINDING_JSONRPC, "http://localhost:1111")]);
803 card.capabilities.extensions = Some(
804 extension_uris
805 .iter()
806 .map(|uri| a2a_protocol_types::AgentExtension::new(*uri))
807 .collect(),
808 );
809 card
810 }
811
812 fn peer_honours(card: &AgentCard) -> bool {
813 ClientBuilder::from_card_preferring(card, &[BINDING_JSONRPC.into()])
814 .expect("from_card_preferring")
815 .peer_honours_idempotency
816 }
817
818 #[test]
819 fn a_card_advertising_the_idempotency_extension_marks_the_peer_as_honouring_keys() {
820 let card = card_advertising(&[a2a_protocol_types::idempotency::IDEMPOTENCY_EXTENSION_URI]);
821
822 assert!(
823 peer_honours(&card),
824 "the card advertises the extension, so a keyed send is retryable"
825 );
826 }
827
828 #[test]
829 fn a_card_advertising_only_other_extensions_leaves_the_peer_unmarked() {
830 let card = card_advertising(&[
834 "https://example.com/extensions/unrelated/v1",
835 "https://a2a-rust.com/extensions/idempotency/v2",
836 ]);
837
838 assert!(
839 !peer_honours(&card),
840 "no extension on this card is the idempotency extension; marking \
841 the peer as honouring keys would make the client retry a send \
842 that the peer runs a second time"
843 );
844 }
845
846 #[test]
847 fn a_card_advertising_no_extensions_at_all_leaves_the_peer_unmarked() {
848 let card = card_with(vec![iface(BINDING_JSONRPC, "http://localhost:1111")]);
851 assert!(card.capabilities.extensions.is_none());
852
853 assert!(!peer_honours(&card));
854 }
855
856 #[test]
857 fn builder_from_card_uses_card_url() {
858 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
859
860 let card = AgentCard {
861 url: None,
862 name: "test".into(),
863 version: "1.0".into(),
864 description: "A test agent".into(),
865 supported_interfaces: vec![AgentInterface {
866 url: "http://localhost:9090".into(),
867 protocol_binding: "JSONRPC".into(),
868 protocol_version: "1.0.0".into(),
869 tenant: None,
870 }],
871 provider: None,
872 icon_url: None,
873 documentation_url: None,
874 capabilities: AgentCapabilities::none(),
875 security_schemes: None,
876 security_requirements: None,
877 default_input_modes: vec![],
878 default_output_modes: vec![],
879 skills: vec![],
880 signatures: None,
881 };
882
883 let client = ClientBuilder::from_card(&card)
884 .unwrap()
885 .build()
886 .expect("build");
887 let _ = client;
888 }
889
890 #[test]
891 fn builder_with_timeout_sets_config() {
892 let client = ClientBuilder::new("http://localhost:8080")
893 .with_timeout(Duration::from_secs(60))
894 .build()
895 .expect("build");
896 assert_eq!(client.config().request_timeout, Duration::from_secs(60));
897 }
898
899 #[test]
900 fn builder_from_card_empty_interfaces_returns_error() {
901 use a2a_protocol_types::{AgentCapabilities, AgentCard};
902
903 let card = AgentCard {
904 url: None,
905 name: "empty".into(),
906 version: "1.0".into(),
907 description: "No interfaces".into(),
908 supported_interfaces: vec![],
909 provider: None,
910 icon_url: None,
911 documentation_url: None,
912 capabilities: AgentCapabilities::none(),
913 security_schemes: None,
914 security_requirements: None,
915 default_input_modes: vec![],
916 default_output_modes: vec![],
917 skills: vec![],
918 signatures: None,
919 };
920
921 let result = ClientBuilder::from_card(&card);
922 assert!(result.is_err(), "empty interfaces should return error");
923 }
924
925 #[test]
926 fn builder_with_return_immediately() {
927 let client = ClientBuilder::new("http://localhost:8080")
928 .with_return_immediately(true)
929 .build()
930 .expect("build");
931 assert!(client.config().return_immediately);
932 }
933
934 #[test]
935 fn builder_with_history_length() {
936 let client = ClientBuilder::new("http://localhost:8080")
937 .with_history_length(10)
938 .build()
939 .expect("build");
940 assert_eq!(client.config().history_length, Some(10));
941 }
942
943 #[test]
944 fn builder_debug_contains_fields() {
945 let builder = ClientBuilder::new("http://localhost:8080");
946 let debug = format!("{builder:?}");
947 assert!(
948 debug.contains("ClientBuilder"),
949 "debug output missing struct name: {debug}"
950 );
951 assert!(
952 debug.contains("http://localhost:8080"),
953 "debug output missing endpoint: {debug}"
954 );
955 }
956
957 #[test]
960 fn builder_from_card_mismatched_version() {
961 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
962
963 let card = AgentCard {
964 url: None,
965 name: "mismatch".into(),
966 version: "1.0".into(),
967 description: "Version mismatch test".into(),
968 supported_interfaces: vec![AgentInterface {
969 url: "http://localhost:9091".into(),
970 protocol_binding: "JSONRPC".into(),
971 protocol_version: "99.0.0".into(), tenant: None,
973 }],
974 provider: None,
975 icon_url: None,
976 documentation_url: None,
977 capabilities: AgentCapabilities::none(),
978 security_schemes: None,
979 security_requirements: None,
980 default_input_modes: vec![],
981 default_output_modes: vec![],
982 skills: vec![],
983 signatures: None,
984 };
985
986 let builder = ClientBuilder::from_card(&card).unwrap();
987 assert_eq!(builder.endpoint, "http://localhost:9091");
988 }
989
990 #[test]
993 fn version_mismatch_matching_major_returns_none() {
994 assert_eq!(protocol_version_mismatch("1.0.0"), None);
995 assert_eq!(protocol_version_mismatch("1.2.3"), None);
996 assert_eq!(protocol_version_mismatch("1"), None);
997 }
998
999 #[test]
1000 fn version_mismatch_returns_original_on_mismatch() {
1001 assert_eq!(protocol_version_mismatch("0.5.0"), Some("0.5.0"));
1002 assert_eq!(protocol_version_mismatch("2.0.0"), Some("2.0.0"));
1003 assert_eq!(protocol_version_mismatch("99.0.0"), Some("99.0.0"));
1004 }
1005
1006 #[test]
1007 fn version_mismatch_empty_is_compatible() {
1008 assert_eq!(protocol_version_mismatch(""), None);
1010 }
1011
1012 #[test]
1013 fn version_mismatch_unparseable_is_incompatible() {
1014 assert_eq!(
1015 protocol_version_mismatch("not-a-version"),
1016 Some("not-a-version")
1017 );
1018 assert_eq!(protocol_version_mismatch("v1.0.0"), Some("v1.0.0"));
1019 assert_eq!(protocol_version_mismatch("1-preview"), Some("1-preview"));
1020 }
1021
1022 #[test]
1029 fn builder_from_card_preserves_tenant() {
1030 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
1031
1032 let card = AgentCard {
1033 url: None,
1034 name: "multi-tenant".into(),
1035 version: "1.0".into(),
1036 description: "Multi-tenant agent".into(),
1037 supported_interfaces: vec![AgentInterface {
1038 url: "http://localhost:9092".into(),
1039 protocol_binding: "JSONRPC".into(),
1040 protocol_version: "1.0.0".into(),
1041 tenant: Some("tenant-42".into()),
1042 }],
1043 provider: None,
1044 icon_url: None,
1045 documentation_url: None,
1046 capabilities: AgentCapabilities::none(),
1047 security_schemes: None,
1048 security_requirements: None,
1049 default_input_modes: vec![],
1050 default_output_modes: vec![],
1051 skills: vec![],
1052 signatures: None,
1053 };
1054
1055 let builder = ClientBuilder::from_card(&card).expect("from_card");
1056 assert_eq!(
1057 builder.config.tenant.as_deref(),
1058 Some("tenant-42"),
1059 "tenant from AgentInterface must be propagated to ClientConfig"
1060 );
1061 }
1062
1063 #[test]
1064 fn builder_from_card_none_tenant_stays_none() {
1065 use a2a_protocol_types::{AgentCapabilities, AgentCard, AgentInterface};
1066
1067 let card = AgentCard {
1068 url: None,
1069 name: "no-tenant".into(),
1070 version: "1.0".into(),
1071 description: String::new(),
1072 supported_interfaces: vec![AgentInterface {
1073 url: "http://localhost:9093".into(),
1074 protocol_binding: "JSONRPC".into(),
1075 protocol_version: "1.0.0".into(),
1076 tenant: None,
1077 }],
1078 provider: None,
1079 icon_url: None,
1080 documentation_url: None,
1081 capabilities: AgentCapabilities::none(),
1082 security_schemes: None,
1083 security_requirements: None,
1084 default_input_modes: vec![],
1085 default_output_modes: vec![],
1086 skills: vec![],
1087 signatures: None,
1088 };
1089
1090 let builder = ClientBuilder::from_card(&card).expect("from_card");
1091 assert!(builder.config.tenant.is_none());
1092 }
1093
1094 #[test]
1096 fn builder_with_connection_timeout_and_retry_policy() {
1097 use crate::retry::RetryPolicy;
1098
1099 let client = ClientBuilder::new("http://localhost:8080")
1100 .with_connection_timeout(Duration::from_secs(5))
1101 .with_retry_policy(RetryPolicy::default())
1102 .build()
1103 .expect("build");
1104 assert_eq!(client.config().connection_timeout, Duration::from_secs(5));
1105 }
1106
1107 #[test]
1109 fn builder_with_stream_connect_timeout() {
1110 let client = ClientBuilder::new("http://localhost:8080")
1111 .with_stream_connect_timeout(Duration::from_secs(15))
1112 .build()
1113 .expect("build");
1114 assert_eq!(
1115 client.config().stream_connect_timeout,
1116 Duration::from_secs(15)
1117 );
1118 }
1119}