Skip to content

Commit f90655a

Browse files
authored
Merge pull request #20 from agntcy/fix/19-jsonrpc-agent-card-interop
fix: align jsonrpc and agent-card interop
2 parents 8198656 + 4c13bff commit f90655a

File tree

6 files changed

+221
-36
lines changed

6 files changed

+221
-36
lines changed

a2a-client/src/agent_card.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,59 @@ impl AgentCardResolver {
4343
.map_err(|e| A2AError::internal(format!("failed to parse agent card: {e}")))
4444
}
4545
}
46+
47+
#[cfg(test)]
48+
mod tests {
49+
use super::*;
50+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
51+
use tokio::net::TcpListener;
52+
53+
async fn spawn_agent_card_server(body: &'static str) -> String {
54+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
55+
let addr = listener.local_addr().unwrap();
56+
57+
tokio::spawn(async move {
58+
let (mut socket, _) = listener.accept().await.unwrap();
59+
let mut buffer = [0_u8; 4096];
60+
let _ = socket.read(&mut buffer).await;
61+
62+
let response = format!(
63+
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
64+
body.len(),
65+
body,
66+
);
67+
socket.write_all(response.as_bytes()).await.unwrap();
68+
});
69+
70+
format!("http://{addr}")
71+
}
72+
73+
#[tokio::test]
74+
async fn test_resolve_accepts_null_skills() {
75+
let server = spawn_agent_card_server(
76+
r#"{
77+
"name": "Test Agent",
78+
"description": "A test agent",
79+
"version": "1.0.0",
80+
"supportedInterfaces": [
81+
{
82+
"url": "http://127.0.0.1:3000/jsonrpc",
83+
"protocolBinding": "JSONRPC",
84+
"protocolVersion": "1.0"
85+
}
86+
],
87+
"capabilities": { "streaming": true },
88+
"defaultInputModes": ["text/plain"],
89+
"defaultOutputModes": ["text/plain"],
90+
"skills": null
91+
}"#,
92+
)
93+
.await;
94+
95+
let resolver = AgentCardResolver::new(None);
96+
let card = resolver.resolve(&server).await.unwrap();
97+
98+
assert!(card.skills.is_empty());
99+
assert_eq!(card.supported_interfaces[0].protocol_binding, "JSONRPC");
100+
}
101+
}

a2a-client/src/middleware.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,28 @@ mod tests {
7878
async fn test_logging_interceptor_before() {
7979
let interceptor = LoggingInterceptor;
8080
let mut params = ServiceParams::new();
81-
let result = interceptor.before("message.send", &mut params).await;
81+
let result = interceptor
82+
.before(a2a::jsonrpc::methods::SEND_MESSAGE, &mut params)
83+
.await;
8284
assert!(result.is_ok());
8385
}
8486

8587
#[tokio::test]
8688
async fn test_logging_interceptor_after_ok() {
8789
let interceptor = LoggingInterceptor;
88-
let result = interceptor.after("message.send", &Ok(())).await;
90+
let result = interceptor
91+
.after(a2a::jsonrpc::methods::SEND_MESSAGE, &Ok(()))
92+
.await;
8993
assert!(result.is_ok());
9094
}
9195

9296
#[tokio::test]
9397
async fn test_logging_interceptor_after_err() {
9498
let interceptor = LoggingInterceptor;
9599
let err = Err(A2AError::internal("boom"));
96-
let result = interceptor.after("message.send", &err).await;
100+
let result = interceptor
101+
.after(a2a::jsonrpc::methods::SEND_MESSAGE, &err)
102+
.await;
97103
assert!(result.is_ok());
98104
}
99105
}

a2a-server/src/jsonrpc.rs

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -291,16 +291,31 @@ mod tests {
291291
"parts": [{"text": "hello"}]
292292
}
293293
});
294-
let resp = post_jsonrpc(app, "message.send", params).await;
294+
let resp = post_jsonrpc(app, methods::SEND_MESSAGE, params).await;
295295
assert!(resp.error.is_none(), "unexpected error: {:?}", resp.error);
296296
assert!(resp.result.is_some());
297297
}
298298

299+
#[tokio::test]
300+
async fn test_send_message_legacy_method_rejected() {
301+
let app = make_app();
302+
let params = serde_json::json!({
303+
"message": {
304+
"messageId": "m1",
305+
"role": "ROLE_USER",
306+
"parts": [{"text": "hello"}]
307+
}
308+
});
309+
let resp = post_jsonrpc(app, "message.send", params).await;
310+
assert!(resp.error.is_some(), "unexpected result: {:?}", resp.result);
311+
assert_eq!(resp.error.unwrap().code, error_code::METHOD_NOT_FOUND);
312+
}
313+
299314
#[tokio::test]
300315
async fn test_get_task_not_found() {
301316
let app = make_app();
302317
let params = serde_json::json!({"id": "nonexistent"});
303-
let resp = post_jsonrpc(app, "tasks.get", params).await;
318+
let resp = post_jsonrpc(app, methods::GET_TASK, params).await;
304319
assert!(resp.error.is_some());
305320
assert_eq!(resp.error.unwrap().code, error_code::TASK_NOT_FOUND);
306321
}
@@ -326,7 +341,7 @@ mod tests {
326341
let rpc = serde_json::json!({
327342
"jsonrpc": "1.0",
328343
"id": 1,
329-
"method": "message.send",
344+
"method": methods::SEND_MESSAGE,
330345
"params": {}
331346
});
332347
let body = serde_json::to_string(&rpc).unwrap();
@@ -346,7 +361,7 @@ mod tests {
346361
async fn test_invalid_params() {
347362
let app = make_app();
348363
let params = serde_json::json!({"bogus": true});
349-
let resp = post_jsonrpc(app, "message.send", params).await;
364+
let resp = post_jsonrpc(app, methods::SEND_MESSAGE, params).await;
350365
assert!(resp.error.is_some());
351366
assert_eq!(resp.error.unwrap().code, error_code::PARSE_ERROR);
352367
}
@@ -355,15 +370,15 @@ mod tests {
355370
async fn test_list_tasks() {
356371
let app = make_app();
357372
let params = serde_json::json!({});
358-
let resp = post_jsonrpc(app, "tasks.list", params).await;
373+
let resp = post_jsonrpc(app, methods::LIST_TASKS, params).await;
359374
assert!(resp.result.is_some());
360375
}
361376

362377
#[tokio::test]
363378
async fn test_cancel_task_not_found() {
364379
let app = make_app();
365380
let params = serde_json::json!({"id": "nonexistent"});
366-
let resp = post_jsonrpc(app, "tasks.cancel", params).await;
381+
let resp = post_jsonrpc(app, methods::CANCEL_TASK, params).await;
367382
assert!(resp.error.is_some());
368383
}
369384

@@ -376,7 +391,7 @@ mod tests {
376391
"url": "http://example.com/callback"
377392
}
378393
});
379-
let resp = post_jsonrpc(app, "tasks.pushNotificationConfig.create", params).await;
394+
let resp = post_jsonrpc(app, methods::CREATE_PUSH_CONFIG, params).await;
380395
// May fail since task doesn't exist, but method is dispatched
381396
assert!(resp.error.is_some() || resp.result.is_some());
382397
}
@@ -388,7 +403,7 @@ mod tests {
388403
"taskId": "t1",
389404
"id": "cfg1"
390405
});
391-
let resp = post_jsonrpc(app, "tasks.pushNotificationConfig.get", params).await;
406+
let resp = post_jsonrpc(app, methods::GET_PUSH_CONFIG, params).await;
392407
assert!(resp.error.is_some() || resp.result.is_some());
393408
}
394409

@@ -398,7 +413,7 @@ mod tests {
398413
let params = serde_json::json!({
399414
"taskId": "t1"
400415
});
401-
let resp = post_jsonrpc(app, "tasks.pushNotificationConfig.list", params).await;
416+
let resp = post_jsonrpc(app, methods::LIST_PUSH_CONFIGS, params).await;
402417
assert!(resp.error.is_some() || resp.result.is_some());
403418
}
404419

@@ -409,15 +424,15 @@ mod tests {
409424
"taskId": "t1",
410425
"id": "cfg1"
411426
});
412-
let resp = post_jsonrpc(app, "tasks.pushNotificationConfig.delete", params).await;
427+
let resp = post_jsonrpc(app, methods::DELETE_PUSH_CONFIG, params).await;
413428
assert!(resp.error.is_some() || resp.result.is_some());
414429
}
415430

416431
#[tokio::test]
417432
async fn test_get_extended_agent_card() {
418433
let app = make_app();
419434
let params = serde_json::json!({});
420-
let resp = post_jsonrpc(app, "agent.extendedCard.get", params).await;
435+
let resp = post_jsonrpc(app, methods::GET_EXTENDED_AGENT_CARD, params).await;
421436
// DefaultRequestHandler returns NotSupported
422437
assert!(resp.error.is_some() || resp.result.is_some());
423438
}
@@ -432,7 +447,11 @@ mod tests {
432447
"parts": [{"text": "hello"}]
433448
}
434449
});
435-
let rpc = JsonRpcRequest::new(JsonRpcId::Number(1), "message.stream", Some(body));
450+
let rpc = JsonRpcRequest::new(
451+
JsonRpcId::Number(1),
452+
methods::SEND_STREAMING_MESSAGE,
453+
Some(body),
454+
);
436455
let req = Request::builder()
437456
.uri("/")
438457
.method("POST")
@@ -450,7 +469,7 @@ mod tests {
450469
let app = make_app();
451470
let rpc = JsonRpcRequest::new(
452471
JsonRpcId::Number(1),
453-
"tasks.subscribe",
472+
methods::SUBSCRIBE_TO_TASK,
454473
Some(serde_json::json!({"id": "t1"})),
455474
);
456475
let req = Request::builder()
@@ -464,6 +483,35 @@ mod tests {
464483
assert_eq!(resp.status(), StatusCode::OK);
465484
}
466485

486+
#[tokio::test]
487+
async fn test_streaming_send_message_legacy_method_rejected() {
488+
let app = make_app();
489+
let body = serde_json::json!({
490+
"message": {
491+
"messageId": "m1",
492+
"role": "ROLE_USER",
493+
"parts": [{"text": "hello"}]
494+
}
495+
});
496+
let rpc = JsonRpcRequest::new(JsonRpcId::Number(1), "message.stream", Some(body));
497+
let req = Request::builder()
498+
.uri("/")
499+
.method("POST")
500+
.header("content-type", "application/json")
501+
.header("accept", "text/event-stream")
502+
.body(Body::from(serde_json::to_string(&rpc).unwrap()))
503+
.unwrap();
504+
let resp = app.oneshot(req).await.unwrap();
505+
let body = resp.into_body().collect().await.unwrap().to_bytes();
506+
let rpc_resp: JsonRpcResponse = serde_json::from_slice(&body).unwrap();
507+
assert!(
508+
rpc_resp.error.is_some(),
509+
"unexpected result: {:?}",
510+
rpc_resp.result
511+
);
512+
assert_eq!(rpc_resp.error.unwrap().code, error_code::METHOD_NOT_FOUND);
513+
}
514+
467515
#[tokio::test]
468516
async fn test_streaming_invalid_method() {
469517
let app = make_app();

a2a-server/src/middleware.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ mod tests {
121121
#[test]
122122
fn test_call_context_new() {
123123
let params = ServiceParams::new();
124-
let ctx = CallContext::new("message.send", params);
125-
assert_eq!(ctx.method, "message.send");
124+
let ctx = CallContext::new(a2a::jsonrpc::methods::SEND_MESSAGE, params);
125+
assert_eq!(ctx.method, a2a::jsonrpc::methods::SEND_MESSAGE);
126126
assert!(ctx.tenant.is_none());
127127
assert!(ctx.user.is_none());
128128
}
@@ -166,23 +166,23 @@ mod tests {
166166
#[tokio::test]
167167
async fn test_logging_interceptor_before() {
168168
let interceptor = LoggingInterceptor;
169-
let mut ctx = CallContext::new("message.send", ServiceParams::new());
169+
let mut ctx = CallContext::new(a2a::jsonrpc::methods::SEND_MESSAGE, ServiceParams::new());
170170
let result = interceptor.before(&mut ctx, &Value::Null).await;
171171
assert!(result.is_ok());
172172
}
173173

174174
#[tokio::test]
175175
async fn test_logging_interceptor_after_ok() {
176176
let interceptor = LoggingInterceptor;
177-
let ctx = CallContext::new("message.send", ServiceParams::new());
177+
let ctx = CallContext::new(a2a::jsonrpc::methods::SEND_MESSAGE, ServiceParams::new());
178178
let result = interceptor.after(&ctx, &Ok(Value::Null)).await;
179179
assert!(result.is_ok());
180180
}
181181

182182
#[tokio::test]
183183
async fn test_logging_interceptor_after_err() {
184184
let interceptor = LoggingInterceptor;
185-
let ctx = CallContext::new("message.send", ServiceParams::new());
185+
let ctx = CallContext::new(a2a::jsonrpc::methods::SEND_MESSAGE, ServiceParams::new());
186186
let err: Result<Value, A2AError> = Err(A2AError::internal("boom"));
187187
let result = interceptor.after(&ctx, &err).await;
188188
assert!(result.is_ok());

a2a/src/agent_card.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright AGNTCY Contributors (https://github.com/agntcy)
22
// SPDX-License-Identifier: Apache-2.0
3-
use serde::{Deserialize, Serialize};
3+
use serde::{Deserialize, Deserializer, Serialize};
44
use serde_json::Value;
55
use std::collections::HashMap;
66

@@ -21,6 +21,7 @@ pub struct AgentCard {
2121
pub capabilities: AgentCapabilities,
2222
pub default_input_modes: Vec<String>,
2323
pub default_output_modes: Vec<String>,
24+
#[serde(default, deserialize_with = "deserialize_vec_null_as_default")]
2425
pub skills: Vec<AgentSkill>,
2526

2627
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -42,6 +43,14 @@ pub struct AgentCard {
4243
pub signatures: Option<Vec<AgentCardSignature>>,
4344
}
4445

46+
fn deserialize_vec_null_as_default<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
47+
where
48+
D: Deserializer<'de>,
49+
T: Deserialize<'de>,
50+
{
51+
Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
52+
}
53+
4554
// ---------------------------------------------------------------------------
4655
// AgentInterface
4756
// ---------------------------------------------------------------------------
@@ -577,4 +586,57 @@ mod tests {
577586
let back: AgentCard = serde_json::from_str(&json).unwrap();
578587
assert_eq!(card, back);
579588
}
589+
590+
#[test]
591+
fn test_agent_card_deserializes_null_skills_as_empty() {
592+
let card: AgentCard = serde_json::from_str(
593+
r#"{
594+
"name": "Test Agent",
595+
"description": "A test agent",
596+
"version": "1.0.0",
597+
"supportedInterfaces": [
598+
{
599+
"url": "http://localhost:3000",
600+
"protocolBinding": "JSONRPC",
601+
"protocolVersion": "1.0"
602+
}
603+
],
604+
"capabilities": {
605+
"streaming": true
606+
},
607+
"defaultInputModes": ["text/plain"],
608+
"defaultOutputModes": ["text/plain"],
609+
"skills": null
610+
}"#,
611+
)
612+
.unwrap();
613+
614+
assert!(card.skills.is_empty());
615+
}
616+
617+
#[test]
618+
fn test_agent_card_deserializes_missing_skills_as_empty() {
619+
let card: AgentCard = serde_json::from_str(
620+
r#"{
621+
"name": "Test Agent",
622+
"description": "A test agent",
623+
"version": "1.0.0",
624+
"supportedInterfaces": [
625+
{
626+
"url": "http://localhost:3000",
627+
"protocolBinding": "JSONRPC",
628+
"protocolVersion": "1.0"
629+
}
630+
],
631+
"capabilities": {
632+
"streaming": true
633+
},
634+
"defaultInputModes": ["text/plain"],
635+
"defaultOutputModes": ["text/plain"]
636+
}"#,
637+
)
638+
.unwrap();
639+
640+
assert!(card.skills.is_empty());
641+
}
580642
}

0 commit comments

Comments
 (0)