use super::*;
use crate::error::ServerError;
use a2a_protocol_types::error::A2aError;
#[test]
fn every_served_method_is_qualified_and_anything_else_is_other() {
for m in [
"SendMessage",
"SendStreamingMessage",
"GetTask",
"ListTasks",
"CancelTask",
"SubscribeToTask",
"CreateTaskPushNotificationConfig",
"GetTaskPushNotificationConfig",
"ListTaskPushNotificationConfigs",
"DeleteTaskPushNotificationConfig",
"GetExtendedAgentCard",
] {
assert_eq!(a2a_method(m), format!("lf.a2a.v1.A2AService/{m}"));
}
assert_eq!(
a2a_method("message/stream"),
"lf.a2a.v1.A2AService/SendStreamingMessage"
);
assert_eq!(a2a_method("message/send"), "_OTHER");
assert_eq!(a2a_method("DropTables; --"), "_OTHER");
}
#[test]
fn rpc_system_names_are_the_conventions_values() {
assert_eq!(RpcSystem::JsonRpc.name(), "jsonrpc");
assert_eq!(RpcSystem::Grpc.name(), "grpc");
assert_eq!(RpcSystem::HttpJson.name(), "a2a_http_json");
}
#[test]
fn each_binding_reports_the_status_it_sends() {
let not_found = ServerError::TaskNotFound("t".into());
assert_eq!(not_found.wire_status(RpcSystem::JsonRpc), "-32001");
assert_eq!(not_found.wire_status(RpcSystem::HttpJson), "404");
let busy = ServerError::Overloaded("busy".into());
assert_eq!(busy.wire_status(RpcSystem::HttpJson), "503");
assert_eq!(
A2aError::invalid_params("x").wire_status(RpcSystem::JsonRpc),
"-32602"
);
}
#[cfg(feature = "grpc")]
#[test]
fn grpc_reports_the_status_name_it_sends() {
assert_eq!(
ServerError::TaskNotFound("t".into()).wire_status(RpcSystem::Grpc),
"NOT_FOUND"
);
assert_eq!(
ServerError::Overloaded("busy".into()).wire_status(RpcSystem::Grpc),
"RESOURCE_EXHAUSTED"
);
}
type Seen = Arc<std::sync::Mutex<Vec<(Option<String>, Option<String>)>>>;
struct Recording(Seen);
impl Metrics for Recording {
fn on_rpc_call(&self, call: &RpcCall<'_>) {
self.0.lock().unwrap().push((
call.method.map(str::to_owned),
call.error_type.map(str::to_owned),
));
}
}
fn handler(seen: &Seen) -> RequestHandler {
struct Idle;
crate::agent_executor!(Idle, |_ctx, _queue| async { Ok(()) });
crate::RequestHandlerBuilder::new(Idle)
.with_metrics(Recording(Arc::clone(seen)))
.build()
.unwrap()
}
#[tokio::test]
async fn a_call_dropped_in_flight_is_recorded_as_cancelled() {
let seen = Seen::default();
let handler = handler(&seen);
let call = ServerSpan::open(&handler, RpcSystem::JsonRpc, "GetTask", None)
.run(std::future::pending::<Result<(), ServerError>>());
assert!(
tokio::time::timeout(Duration::from_millis(10), call)
.await
.is_err()
);
assert_eq!(
*seen.lock().unwrap(),
[(
Some("lf.a2a.v1.A2AService/GetTask".to_owned()),
Some("cancelled".to_owned())
)]
);
}
#[tokio::test]
async fn a_finished_call_is_recorded_once() {
let seen = Seen::default();
let handler = handler(&seen);
let _ = ServerSpan::open(&handler, RpcSystem::HttpJson, "ListTasks", None)
.run(async { Ok::<_, ServerError>(()) })
.await;
assert_eq!(
*seen.lock().unwrap(),
[(Some("lf.a2a.v1.A2AService/ListTasks".to_owned()), None)]
);
}
#[cfg(feature = "tracing")]
#[derive(Clone, Default)]
struct Created(Arc<std::sync::Mutex<Vec<String>>>);
#[cfg(feature = "tracing")]
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Created {
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
_id: &tracing::span::Id,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
self.0
.lock()
.unwrap()
.push(attrs.metadata().name().to_owned());
}
}
#[cfg(feature = "tracing")]
fn spans_created_under(policy: InboundTracePolicy) -> Vec<String> {
use tracing_subscriber::layer::SubscriberExt as _;
struct Idle;
crate::agent_executor!(Idle, |_ctx, _queue| async { Ok(()) });
let created = Created::default();
let subscriber = tracing_subscriber::registry().with(created.clone());
let seen = Seen::default();
let handler = crate::RequestHandlerBuilder::new(Idle)
.with_metrics(Recording(Arc::clone(&seen)))
.with_inbound_trace_policy(policy)
.build()
.unwrap();
let runtime = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
tracing::subscriber::with_default(subscriber, || {
runtime.block_on(async {
ServerSpan::open(&handler, RpcSystem::JsonRpc, "SendMessage", None)
.run_with(
async {
tokio::spawn(in_executor_span(
"t",
"c",
in_child_span("a2a.process_events", async {}),
))
.await
.map_err(|e| ServerError::Internal(e.to_string()))
},
|_| tokio::spawn(in_child_span("a2a.sse", async {})),
)
.await
.await
.unwrap();
});
});
created.0.lock().unwrap().clone()
}
#[cfg(feature = "tracing")]
#[test]
fn a_dropped_trace_records_no_span_for_the_call_or_its_tasks() {
assert_eq!(
spans_created_under(InboundTracePolicy::Continue).len(),
4,
"the control: a traced call and its three tasks"
);
assert_eq!(
spans_created_under(InboundTracePolicy::Drop),
Vec::<String>::new()
);
}
#[cfg(feature = "tracing")]
#[test]
fn a_long_method_original_is_cut_on_a_character_boundary() {
assert_eq!(truncated("abc", 8), "abc");
let s = "é".repeat(100); let t = truncated(&s, 5);
assert_eq!(t, "éé");
}
#[cfg(feature = "tracing")]
#[derive(Clone, Default)]
struct Fields(Arc<std::sync::Mutex<std::collections::BTreeMap<String, String>>>);
#[cfg(feature = "tracing")]
struct Collect<'a>(&'a mut std::collections::BTreeMap<String, String>);
#[cfg(feature = "tracing")]
impl tracing::field::Visit for Collect<'_> {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.0.insert(field.name().to_owned(), value.to_owned());
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.insert(field.name().to_owned(), format!("{value:?}"));
}
}
#[cfg(feature = "tracing")]
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Fields {
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
_id: &tracing::span::Id,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
attrs.record(&mut Collect(&mut self.0.lock().unwrap()));
}
fn on_record(
&self,
_id: &tracing::span::Id,
values: &tracing::span::Record<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
values.record(&mut Collect(&mut self.0.lock().unwrap()));
}
}
#[cfg(feature = "tracing")]
fn span_fields(method: &str) -> std::collections::BTreeMap<String, String> {
use tracing_subscriber::layer::SubscriberExt as _;
let fields = Fields::default();
let subscriber = tracing_subscriber::registry().with(fields.clone());
let handler = handler(&Seen::default());
tracing::subscriber::with_default(subscriber, || {
drop(ServerSpan::open(&handler, RpcSystem::JsonRpc, method, None));
});
let recorded = fields.0.lock().unwrap();
recorded.clone()
}
#[cfg(feature = "tracing")]
#[test]
fn the_span_is_named_by_its_method_or_by_its_system_for_other() {
let served = span_fields("GetTask");
assert_eq!(
served.get("otel.name").map(String::as_str),
Some("lf.a2a.v1.A2AService/GetTask")
);
assert_eq!(served.get("rpc.method_original"), None);
let unknown = span_fields("NoSuchMethod");
assert_eq!(
unknown.get("otel.name").map(String::as_str),
Some(RpcSystem::JsonRpc.name())
);
assert_eq!(unknown.get("rpc.method").map(String::as_str), Some(OTHER));
assert_eq!(
unknown.get("rpc.method_original").map(String::as_str),
Some("NoSuchMethod")
);
}
#[cfg(feature = "otel")]
#[test]
fn only_continue_makes_the_callers_traceparent_the_spans_parent() {
use opentelemetry::trace::TracerProvider as _;
use tracing_subscriber::layer::SubscriberExt as _;
const TRACE: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder().build();
let subscriber = tracing_subscriber::registry()
.with(tracing_opentelemetry::layer().with_tracer(provider.tracer("rpc_span")));
let mut headers = HashMap::new();
headers.insert(
"traceparent".to_owned(),
format!("00-{TRACE}-00f067aa0ba902b7-01"),
);
let trace_under = |policy: InboundTracePolicy| -> String {
struct Idle;
crate::agent_executor!(Idle, |_ctx, _queue| async { Ok(()) });
let handler = crate::RequestHandlerBuilder::new(Idle)
.with_inbound_trace_policy(policy)
.build()
.unwrap();
let call = ServerSpan::open(&handler, RpcSystem::JsonRpc, "GetTask", Some(&headers));
let (trace, _, _) = call
.span
.in_scope(current_recorded_span)
.expect("the span is recorded");
format!("{:032x}", u128::from_be_bytes(trace))
};
tracing::subscriber::with_default(subscriber, || {
assert_eq!(trace_under(InboundTracePolicy::Continue), TRACE);
assert_ne!(trace_under(InboundTracePolicy::Restart), TRACE);
});
}
#[test]
fn a_server_span_debugs_as_its_system_and_method() {
let handler = handler(&Seen::default());
let call = ServerSpan::open(&handler, RpcSystem::JsonRpc, "GetTask", None);
assert_eq!(
format!("{call:?}"),
r#"ServerSpan { system: JsonRpc, method: "lf.a2a.v1.A2AService/GetTask", .. }"#
);
}