Skip to main content

a2a_server/
rest.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use std::sync::Arc;
4
5use a2a::*;
6use a2a_pb::protojson_conv::{self, ProtoJsonPayload};
7use axum::{
8    Json,
9    extract::{Path, Query, State},
10    http::StatusCode,
11    response::IntoResponse,
12};
13use chrono::{DateTime, Utc};
14use futures::{StreamExt, stream::BoxStream};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::HashMap;
18
19use crate::handler::RequestHandler;
20use crate::middleware::ServiceParams;
21use crate::sse;
22
23const REST_SEND_MESSAGE_PATH: &str = "/message:send";
24const REST_SEND_MESSAGE_LEGACY_PATH: &str = "/message/send";
25const REST_STREAM_MESSAGE_PATH: &str = "/message:stream";
26const REST_STREAM_MESSAGE_LEGACY_PATH: &str = "/message/stream";
27const REST_EXTENDED_AGENT_CARD_PATH: &str = "/extendedAgentCard";
28const REST_EXTENDED_AGENT_CARD_LEGACY_PATH: &str = "/agent-card/extended";
29const REST_PUSH_CONFIGS_PATH: &str = "/tasks/{id}/pushNotificationConfigs";
30const REST_PUSH_CONFIGS_LEGACY_PATH: &str = "/tasks/{id}/push-configs";
31const REST_PUSH_CONFIG_PATH: &str = "/tasks/{id}/pushNotificationConfigs/{config_id}";
32const REST_PUSH_CONFIG_LEGACY_PATH: &str = "/tasks/{id}/push-configs/{config_id}";
33#[derive(Serialize)]
34struct RestErrorEnvelope {
35    error: RestErrorStatus,
36}
37
38#[derive(Serialize)]
39struct RestErrorStatus {
40    code: u16,
41    status: &'static str,
42    message: String,
43    details: Vec<TypedDetail>,
44}
45
46/// Shared state for REST handlers.
47pub struct RestState<H: RequestHandler> {
48    pub handler: Arc<H>,
49}
50
51impl<H: RequestHandler> Clone for RestState<H> {
52    fn clone(&self) -> Self {
53        RestState {
54            handler: self.handler.clone(),
55        }
56    }
57}
58
59/// Create an axum router for the REST/HTTP+JSON protocol binding.
60pub fn rest_router<H: RequestHandler>(handler: Arc<H>) -> axum::Router {
61    let state = RestState { handler };
62    axum::Router::new()
63        .route(
64            REST_SEND_MESSAGE_PATH,
65            axum::routing::post(handle_send_message::<H>),
66        )
67        .route(
68            REST_SEND_MESSAGE_LEGACY_PATH,
69            axum::routing::post(handle_send_message::<H>),
70        )
71        .route(
72            REST_STREAM_MESSAGE_PATH,
73            axum::routing::post(handle_stream_message::<H>),
74        )
75        .route(
76            REST_STREAM_MESSAGE_LEGACY_PATH,
77            axum::routing::post(handle_stream_message::<H>),
78        )
79        .route(
80            "/tasks/{id}",
81            axum::routing::get(handle_get_task_or_subscribe::<H>)
82                .post(handle_post_task_action::<H>),
83        )
84        .route("/tasks", axum::routing::get(handle_list_tasks::<H>))
85        .route(
86            "/tasks/{id}/cancel",
87            axum::routing::post(handle_cancel_task_legacy::<H>),
88        )
89        .route(
90            "/tasks/{id}/subscribe",
91            axum::routing::get(handle_subscribe_to_task_legacy::<H>)
92                .post(handle_subscribe_to_task_legacy::<H>),
93        )
94        .route(
95            REST_PUSH_CONFIGS_PATH,
96            axum::routing::post(handle_create_push_config::<H>).get(handle_list_push_configs::<H>),
97        )
98        .route(
99            REST_PUSH_CONFIGS_LEGACY_PATH,
100            axum::routing::post(handle_create_push_config::<H>).get(handle_list_push_configs::<H>),
101        )
102        .route(
103            REST_PUSH_CONFIG_PATH,
104            axum::routing::get(handle_get_push_config::<H>).delete(handle_delete_push_config::<H>),
105        )
106        .route(
107            REST_PUSH_CONFIG_LEGACY_PATH,
108            axum::routing::get(handle_get_push_config::<H>).delete(handle_delete_push_config::<H>),
109        )
110        .route(
111            REST_EXTENDED_AGENT_CARD_PATH,
112            axum::routing::get(handle_get_extended_agent_card::<H>),
113        )
114        .route(
115            REST_EXTENDED_AGENT_CARD_LEGACY_PATH,
116            axum::routing::get(handle_get_extended_agent_card::<H>),
117        )
118        .with_state(state)
119}
120
121async fn handle_send_message<H: RequestHandler>(
122    State(state): State<RestState<H>>,
123    Json(raw_req): Json<Value>,
124) -> impl IntoResponse {
125    let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
126        Ok(req) => req,
127        Err(e) => return rest_payload_error_response(e),
128    };
129    let params = ServiceParams::new();
130    match state.handler.send_message(&params, req).await {
131        Ok(resp) => protojson_json_response(&resp),
132        Err(e) => rest_error_response(e),
133    }
134}
135
136async fn handle_stream_message<H: RequestHandler>(
137    State(state): State<RestState<H>>,
138    Json(raw_req): Json<Value>,
139) -> impl IntoResponse {
140    let req = match protojson_conv::from_value::<SendMessageRequest>(raw_req) {
141        Ok(req) => req,
142        Err(e) => return rest_payload_error_response(e),
143    };
144    let params = ServiceParams::new();
145    match state.handler.send_streaming_message(&params, req).await {
146        Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
147        Err(e) => rest_error_response(e),
148    }
149}
150
151#[derive(Deserialize)]
152pub struct GetTaskQuery {
153    #[serde(default, rename = "historyLength")]
154    pub history_length: Option<i32>,
155}
156
157async fn handle_get_task_inner<H: RequestHandler>(
158    state: RestState<H>,
159    id: String,
160    query: GetTaskQuery,
161) -> axum::response::Response {
162    let params = ServiceParams::new();
163    let req = GetTaskRequest {
164        id,
165        history_length: query.history_length,
166        tenant: None,
167    };
168    match state.handler.get_task(&params, req).await {
169        Ok(task) => protojson_json_response(&task),
170        Err(e) => rest_error_response(e),
171    }
172}
173
174async fn handle_get_task_or_subscribe<H: RequestHandler>(
175    State(state): State<RestState<H>>,
176    Path(id): Path<String>,
177    Query(query): Query<GetTaskQuery>,
178) -> impl IntoResponse {
179    if let Some(task_id) = id.strip_suffix(":subscribe") {
180        return handle_subscribe_to_task_inner(state, task_id.to_string()).await;
181    }
182
183    handle_get_task_inner(state, id, query).await
184}
185
186#[derive(Deserialize)]
187pub struct ListTasksQuery {
188    #[serde(default, rename = "contextId")]
189    pub context_id: Option<String>,
190    #[serde(default)]
191    pub status: Option<String>,
192    #[serde(default, rename = "pageSize")]
193    pub page_size: Option<i32>,
194    #[serde(default, rename = "pageToken")]
195    pub page_token: Option<String>,
196    #[serde(default, rename = "historyLength")]
197    pub history_length: Option<i32>,
198
199    #[serde(default, rename = "statusTimestampAfter")]
200    pub status_timestamp_after: Option<DateTime<Utc>>,
201
202    #[serde(default, rename = "includeArtifacts")]
203    pub include_artifacts: Option<bool>,
204}
205
206async fn handle_list_tasks<H: RequestHandler>(
207    State(state): State<RestState<H>>,
208    Query(query): Query<ListTasksQuery>,
209) -> impl IntoResponse {
210    let params = ServiceParams::new();
211    let status = query
212        .status
213        .and_then(|s| serde_json::from_value::<TaskState>(serde_json::Value::String(s)).ok());
214    let req = ListTasksRequest {
215        context_id: query.context_id,
216        status,
217        page_size: query.page_size,
218        page_token: query.page_token,
219        history_length: query.history_length,
220        status_timestamp_after: query.status_timestamp_after,
221        include_artifacts: query.include_artifacts,
222        tenant: None,
223    };
224    match state.handler.list_tasks(&params, req).await {
225        Ok(resp) => protojson_json_response(&resp),
226        Err(e) => rest_error_response(e),
227    }
228}
229
230async fn handle_cancel_task_inner<H: RequestHandler>(
231    state: RestState<H>,
232    id: String,
233) -> axum::response::Response {
234    let params = ServiceParams::new();
235    let req = CancelTaskRequest {
236        id,
237        metadata: None,
238        tenant: None,
239    };
240    match state.handler.cancel_task(&params, req).await {
241        Ok(task) => protojson_json_response(&task),
242        Err(e) => rest_error_response(e),
243    }
244}
245
246async fn handle_post_task_action<H: RequestHandler>(
247    State(state): State<RestState<H>>,
248    Path(id): Path<String>,
249) -> impl IntoResponse {
250    if let Some(task_id) = id.strip_suffix(":cancel") {
251        return handle_cancel_task_inner(state, task_id.to_string()).await;
252    }
253    if let Some(task_id) = id.strip_suffix(":subscribe") {
254        return handle_subscribe_to_task_inner(state, task_id.to_string()).await;
255    }
256
257    rest_error_response(A2AError::invalid_request("unsupported task action"))
258}
259
260async fn handle_cancel_task_legacy<H: RequestHandler>(
261    State(state): State<RestState<H>>,
262    Path(id): Path<String>,
263) -> impl IntoResponse {
264    handle_cancel_task_inner(state, id).await
265}
266
267async fn handle_subscribe_to_task_inner<H: RequestHandler>(
268    state: RestState<H>,
269    id: String,
270) -> axum::response::Response {
271    let params = ServiceParams::new();
272    let req = SubscribeToTaskRequest { id, tenant: None };
273    match state.handler.subscribe_to_task(&params, req).await {
274        Ok(stream) => sse::sse_from_stream(protojson_stream(stream)).into_response(),
275        Err(e) => rest_error_response(e),
276    }
277}
278
279async fn handle_subscribe_to_task_legacy<H: RequestHandler>(
280    State(state): State<RestState<H>>,
281    Path(id): Path<String>,
282) -> impl IntoResponse {
283    handle_subscribe_to_task_inner(state, id).await
284}
285
286#[derive(Deserialize)]
287pub struct ListPushConfigsQuery {
288    #[serde(default, rename = "pageSize")]
289    pub page_size: Option<i32>,
290
291    #[serde(default, rename = "pageToken")]
292    pub page_token: Option<String>,
293}
294
295async fn handle_create_push_config<H: RequestHandler>(
296    State(state): State<RestState<H>>,
297    Path(id): Path<String>,
298    Json(raw_config): Json<Value>,
299) -> impl IntoResponse {
300    let req = match parse_create_push_config_request(id, raw_config) {
301        Ok(req) => req,
302        Err(e) => return rest_payload_error_response(e),
303    };
304    let params = ServiceParams::new();
305    match state.handler.create_push_config(&params, req).await {
306        Ok(resp) => protojson_json_response(&resp),
307        Err(e) => rest_error_response(e),
308    }
309}
310
311async fn handle_get_push_config<H: RequestHandler>(
312    State(state): State<RestState<H>>,
313    Path((id, config_id)): Path<(String, String)>,
314) -> impl IntoResponse {
315    let params = ServiceParams::new();
316    let req = GetTaskPushNotificationConfigRequest {
317        task_id: id,
318        id: config_id,
319        tenant: None,
320    };
321    match state.handler.get_push_config(&params, req).await {
322        Ok(resp) => protojson_json_response(&resp),
323        Err(e) => rest_error_response(e),
324    }
325}
326
327async fn handle_list_push_configs<H: RequestHandler>(
328    State(state): State<RestState<H>>,
329    Path(id): Path<String>,
330    Query(query): Query<ListPushConfigsQuery>,
331) -> impl IntoResponse {
332    let params = ServiceParams::new();
333    let req = ListTaskPushNotificationConfigsRequest {
334        task_id: id,
335        page_size: query.page_size,
336        page_token: query.page_token,
337        tenant: None,
338    };
339    match state.handler.list_push_configs(&params, req).await {
340        Ok(resp) => protojson_json_response(&resp),
341        Err(e) => rest_error_response(e),
342    }
343}
344
345async fn handle_delete_push_config<H: RequestHandler>(
346    State(state): State<RestState<H>>,
347    Path((id, config_id)): Path<(String, String)>,
348) -> impl IntoResponse {
349    let params = ServiceParams::new();
350    let req = DeleteTaskPushNotificationConfigRequest {
351        task_id: id,
352        id: config_id,
353        tenant: None,
354    };
355    match state.handler.delete_push_config(&params, req).await {
356        Ok(()) => StatusCode::OK.into_response(),
357        Err(e) => rest_error_response(e),
358    }
359}
360
361async fn handle_get_extended_agent_card<H: RequestHandler>(
362    State(state): State<RestState<H>>,
363) -> impl IntoResponse {
364    let params = ServiceParams::new();
365    let req = GetExtendedAgentCardRequest { tenant: None };
366    match state.handler.get_extended_agent_card(&params, req).await {
367        Ok(card) => protojson_json_response(&card),
368        Err(e) => rest_error_response(e),
369    }
370}
371
372fn protojson_json_response<T: ProtoJsonPayload>(value: &T) -> axum::response::Response {
373    match protojson_conv::to_value(value) {
374        Ok(payload) => Json(payload).into_response(),
375        Err(e) => rest_error_response(A2AError::internal(format!(
376            "failed to serialize ProtoJSON payload: {e}"
377        ))),
378    }
379}
380
381fn protojson_stream(
382    stream: BoxStream<'static, Result<StreamResponse, A2AError>>,
383) -> BoxStream<'static, Result<Value, A2AError>> {
384    Box::pin(stream.map(|item| {
385        item.and_then(|value| {
386            protojson_conv::to_value(&value).map_err(|e| {
387                A2AError::internal(format!("failed to serialize ProtoJSON stream payload: {e}"))
388            })
389        })
390    }))
391}
392
393fn parse_create_push_config_request(
394    task_id: String,
395    raw_config: Value,
396) -> Result<TaskPushNotificationConfig, String> {
397    protojson_conv::from_value::<TaskPushNotificationConfig>(raw_config)
398        .map_err(|e| e.to_string())
399        .and_then(|mut config| {
400            if !config.task_id.is_empty() && config.task_id != task_id {
401                Err(format!(
402                    "taskId in request body ({}) did not match task id in path ({task_id})",
403                    config.task_id
404                ))
405            } else {
406                config.task_id = task_id;
407                Ok(config)
408            }
409        })
410}
411
412fn rest_payload_error_response(error: impl std::fmt::Display) -> axum::response::Response {
413    let violation = FieldViolation {
414        field: String::new(),
415        description: error.to_string(),
416    };
417    rest_error_response(
418        A2AError::invalid_params("invalid request body")
419            .with_details(vec![TypedDetail::bad_request(vec![violation])]),
420    )
421}
422
423fn rest_error_response(err: A2AError) -> axum::response::Response {
424    let status =
425        StatusCode::from_u16(err.http_status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
426    let reason = error_reason(err.code);
427    let grpc_status = rest_grpc_status(err.code);
428
429    let mut metadata = HashMap::from([("timestamp".to_string(), Utc::now().to_rfc3339())]);
430    let mut details = Vec::new();
431
432    if let Some(extra) = err.details {
433        for detail in extra {
434            if detail.type_url == errordetails::ERROR_INFO_TYPE {
435                if let Some(Value::Object(meta)) = detail.value.get("metadata") {
436                    for (k, v) in meta {
437                        if let Some(s) = v.as_str() {
438                            metadata.entry(k.clone()).or_insert_with(|| s.to_string());
439                        }
440                    }
441                }
442            } else {
443                details.push(detail);
444            }
445        }
446    }
447
448    details.push(TypedDetail::error_info(
449        reason,
450        errordetails::PROTOCOL_DOMAIN,
451        Some(metadata),
452    ));
453
454    let body = RestErrorEnvelope {
455        error: RestErrorStatus {
456            code: status.as_u16(),
457            status: grpc_status,
458            message: err.message,
459            details,
460        },
461    };
462
463    (status, Json(body)).into_response()
464}
465
466fn rest_grpc_status(code: i32) -> &'static str {
467    match code {
468        error_code::TASK_NOT_FOUND => "NOT_FOUND",
469        error_code::METHOD_NOT_FOUND => "UNIMPLEMENTED",
470        error_code::TASK_NOT_CANCELABLE
471        | error_code::EXTENDED_CARD_NOT_CONFIGURED
472        | error_code::EXTENSION_SUPPORT_REQUIRED
473        | error_code::PUSH_NOTIFICATION_NOT_SUPPORTED
474        | error_code::UNSUPPORTED_OPERATION
475        | error_code::VERSION_NOT_SUPPORTED => "FAILED_PRECONDITION",
476        error_code::CONTENT_TYPE_NOT_SUPPORTED
477        | error_code::PARSE_ERROR
478        | error_code::INVALID_REQUEST
479        | error_code::INVALID_PARAMS => "INVALID_ARGUMENT",
480        _ => "INTERNAL",
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::InMemoryPushConfigStore;
488    use crate::executor::ExecutorContext;
489    use crate::handler::DefaultRequestHandler;
490    use crate::task_store::InMemoryTaskStore;
491    use a2a_pb::protojson_conv;
492    use axum::body::Body;
493    use axum::http::Request;
494    use futures::stream::BoxStream;
495    use http_body_util::BodyExt;
496
497    use crate::test_util::install_crypto_provider;
498    use tower::ServiceExt;
499
500    struct EchoExecutor;
501
502    impl crate::AgentExecutor for EchoExecutor {
503        fn execute(
504            &self,
505            ctx: ExecutorContext,
506        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
507            let task = Task {
508                id: ctx.task_id.clone(),
509                context_id: ctx.context_id.clone(),
510                status: TaskStatus {
511                    state: TaskState::Completed,
512                    message: ctx.message,
513                    timestamp: None,
514                },
515                artifacts: None,
516                history: None,
517                metadata: None,
518            };
519            Box::pin(futures::stream::once(async move {
520                Ok(StreamResponse::Task(task))
521            }))
522        }
523
524        fn cancel(
525            &self,
526            ctx: ExecutorContext,
527        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
528            let task = Task {
529                id: ctx.task_id.clone(),
530                context_id: ctx.context_id.clone(),
531                status: TaskStatus {
532                    state: TaskState::Canceled,
533                    message: None,
534                    timestamp: None,
535                },
536                artifacts: None,
537                history: None,
538                metadata: None,
539            };
540            Box::pin(futures::stream::once(async move {
541                Ok(StreamResponse::Task(task))
542            }))
543        }
544    }
545
546    fn make_app() -> axum::Router {
547        let handler = Arc::new(DefaultRequestHandler::new(
548            EchoExecutor,
549            InMemoryTaskStore::new(),
550        ));
551        rest_router(handler)
552    }
553
554    fn make_push_app() -> axum::Router {
555        install_crypto_provider();
556        let handler = Arc::new(
557            DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
558                .with_push_config_store(InMemoryPushConfigStore::new()),
559        );
560        rest_router(handler)
561    }
562
563    #[tokio::test]
564    async fn test_send_message() {
565        let app = make_app();
566        let body = serde_json::json!({
567            "message": {
568                "messageId": "m1",
569                "role": "ROLE_USER",
570                "parts": [{"text": "hello"}]
571            }
572        });
573        let req = Request::builder()
574            .uri(REST_SEND_MESSAGE_PATH)
575            .method("POST")
576            .header("content-type", "application/json")
577            .body(Body::from(serde_json::to_string(&body).unwrap()))
578            .unwrap();
579        let resp = app.oneshot(req).await.unwrap();
580        assert_eq!(resp.status(), StatusCode::OK);
581    }
582
583    #[tokio::test]
584    async fn test_send_message_legacy_alias() {
585        let app = make_app();
586        let body = serde_json::json!({
587            "message": {
588                "messageId": "m1",
589                "role": "ROLE_USER",
590                "parts": [{"text": "hello"}]
591            }
592        });
593        let req = Request::builder()
594            .uri(REST_SEND_MESSAGE_LEGACY_PATH)
595            .method("POST")
596            .header("content-type", "application/json")
597            .body(Body::from(serde_json::to_string(&body).unwrap()))
598            .unwrap();
599        let resp = app.oneshot(req).await.unwrap();
600        assert_eq!(resp.status(), StatusCode::OK);
601    }
602
603    #[tokio::test]
604    async fn test_get_task_not_found() {
605        let app = make_app();
606        let req = Request::builder()
607            .uri("/tasks/nonexistent")
608            .method("GET")
609            .body(Body::empty())
610            .unwrap();
611        let resp = app.oneshot(req).await.unwrap();
612        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
613    }
614
615    #[tokio::test]
616    async fn test_list_tasks_empty() {
617        let app = make_app();
618        let req = Request::builder()
619            .uri("/tasks")
620            .method("GET")
621            .body(Body::empty())
622            .unwrap();
623        let resp = app.oneshot(req).await.unwrap();
624        assert_eq!(resp.status(), StatusCode::OK);
625        let body = resp.into_body().collect().await.unwrap().to_bytes();
626        let list_resp =
627            protojson_conv::from_str::<ListTasksResponse>(std::str::from_utf8(&body).unwrap())
628                .unwrap();
629        assert!(list_resp.tasks.is_empty());
630    }
631
632    #[tokio::test]
633    async fn test_cancel_task_not_found() {
634        let app = make_app();
635        let req = Request::builder()
636            .uri("/tasks/nonexistent:cancel")
637            .method("POST")
638            .header("content-type", "application/json")
639            .body(Body::empty())
640            .unwrap();
641        let resp = app.oneshot(req).await.unwrap();
642        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
643    }
644
645    #[tokio::test]
646    async fn test_create_push_config_not_supported() {
647        let app = make_app();
648        let body = serde_json::json!({
649            "url": "http://example.com/callback"
650        });
651        let req = Request::builder()
652            .uri("/tasks/t1/pushNotificationConfigs")
653            .method("POST")
654            .header("content-type", "application/json")
655            .body(Body::from(serde_json::to_string(&body).unwrap()))
656            .unwrap();
657        let resp = app.oneshot(req).await.unwrap();
658        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
659    }
660
661    #[tokio::test]
662    async fn test_create_push_config_accepts_raw_config_body() {
663        let app = make_push_app();
664        let body = serde_json::json!({
665            "id": "cfg1",
666            "url": "http://example.com/callback"
667        });
668        let req = Request::builder()
669            .uri("/tasks/t1/pushNotificationConfigs")
670            .method("POST")
671            .header("content-type", "application/json")
672            .body(Body::from(serde_json::to_string(&body).unwrap()))
673            .unwrap();
674        let resp = app.oneshot(req).await.unwrap();
675        assert_eq!(resp.status(), StatusCode::OK);
676        let body = resp.into_body().collect().await.unwrap().to_bytes();
677        let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
678        assert_eq!(payload.task_id, "t1");
679        assert_eq!(payload.id.as_deref(), Some("cfg1"));
680        assert_eq!(payload.url, "http://example.com/callback");
681    }
682
683    #[tokio::test]
684    async fn test_create_push_config_accepts_flat_request_body() {
685        let app = make_push_app();
686        let body = serde_json::json!({
687            "taskId": "t1",
688            "id": "cfg1",
689            "url": "http://example.com/callback"
690        });
691        let req = Request::builder()
692            .uri("/tasks/t1/pushNotificationConfigs")
693            .method("POST")
694            .header("content-type", "application/json")
695            .body(Body::from(serde_json::to_string(&body).unwrap()))
696            .unwrap();
697        let resp = app.oneshot(req).await.unwrap();
698        assert_eq!(resp.status(), StatusCode::OK);
699        let body = resp.into_body().collect().await.unwrap().to_bytes();
700        let payload = serde_json::from_slice::<TaskPushNotificationConfig>(&body).unwrap();
701        assert_eq!(payload.task_id, "t1");
702        assert_eq!(payload.id.as_deref(), Some("cfg1"));
703        assert_eq!(payload.url, "http://example.com/callback");
704    }
705
706    #[tokio::test]
707    async fn test_create_push_config_rejects_mismatched_task_id_in_body() {
708        let app = make_push_app();
709        let body = serde_json::json!({
710            "taskId": "other-task",
711            "url": "http://example.com/callback"
712        });
713        let req = Request::builder()
714            .uri("/tasks/t1/pushNotificationConfigs")
715            .method("POST")
716            .header("content-type", "application/json")
717            .body(Body::from(serde_json::to_string(&body).unwrap()))
718            .unwrap();
719        let resp = app.oneshot(req).await.unwrap();
720        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
721    }
722
723    #[tokio::test]
724    async fn test_get_extended_agent_card() {
725        let app = make_app();
726        let req = Request::builder()
727            .uri(REST_EXTENDED_AGENT_CARD_PATH)
728            .method("GET")
729            .body(Body::empty())
730            .unwrap();
731        let resp = app.oneshot(req).await.unwrap();
732        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
733    }
734
735    #[tokio::test]
736    async fn test_rest_error_response_status_codes() {
737        let resp = rest_error_response(A2AError::task_not_found("t1"));
738        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
739
740        let body = resp.into_body().collect().await.unwrap().to_bytes();
741        let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
742        assert_eq!(payload["error"]["code"], 404);
743        assert_eq!(payload["error"]["status"], "NOT_FOUND");
744        assert_eq!(payload["error"]["details"][0]["reason"], "TASK_NOT_FOUND");
745
746        let resp = rest_error_response(A2AError::internal("boom"));
747        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
748
749        let resp = rest_error_response(A2AError::content_type_not_supported());
750        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
751    }
752
753    #[tokio::test]
754    async fn test_rest_error_response_merges_error_info_metadata() {
755        let existing_info = TypedDetail::error_info(
756            "TASK_NOT_FOUND",
757            errordetails::PROTOCOL_DOMAIN,
758            Some(HashMap::from([("taskId".to_string(), "t99".to_string())])),
759        );
760        let err = A2AError::task_not_found("t99").with_details(vec![existing_info]);
761        let resp = rest_error_response(err);
762        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
763
764        let body = resp.into_body().collect().await.unwrap().to_bytes();
765        let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
766        let details = payload["error"]["details"].as_array().unwrap();
767        // Only one ErrorInfo should be present (merged)
768        assert_eq!(details.len(), 1);
769        assert_eq!(details[0]["@type"], errordetails::ERROR_INFO_TYPE);
770        assert_eq!(details[0]["reason"], "TASK_NOT_FOUND");
771        // Merged metadata should contain both taskId and timestamp
772        assert_eq!(details[0]["metadata"]["taskId"], "t99");
773        assert!(details[0]["metadata"]["timestamp"].is_string());
774    }
775
776    #[tokio::test]
777    async fn test_rest_error_response_preserves_non_error_info_details() {
778        let bad_req = TypedDetail::bad_request(vec![FieldViolation {
779            field: "message.parts".into(),
780            description: "required".into(),
781        }]);
782        let err = A2AError::invalid_params("bad request").with_details(vec![bad_req]);
783        let resp = rest_error_response(err);
784        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
785
786        let body = resp.into_body().collect().await.unwrap().to_bytes();
787        let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
788        let details = payload["error"]["details"].as_array().unwrap();
789        // BadRequest detail + auto-generated ErrorInfo
790        assert_eq!(details.len(), 2);
791        assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
792        assert_eq!(details[0]["fieldViolations"][0]["field"], "message.parts");
793        assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
794        assert_eq!(details[1]["reason"], "INVALID_PARAMS");
795    }
796
797    #[tokio::test]
798    async fn test_rest_payload_error_response() {
799        let resp = rest_payload_error_response("missing field 'parts'");
800        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
801
802        let body = resp.into_body().collect().await.unwrap().to_bytes();
803        let payload: serde_json::Value = serde_json::from_slice(&body).unwrap();
804        assert_eq!(
805            payload["error"]["message"].as_str().unwrap(),
806            "invalid request body"
807        );
808        let details = payload["error"]["details"].as_array().unwrap();
809        // BadRequest detail + ErrorInfo
810        assert_eq!(details.len(), 2);
811        assert_eq!(details[0]["@type"], errordetails::BAD_REQUEST_TYPE);
812        assert_eq!(
813            details[0]["fieldViolations"][0]["description"],
814            "missing field 'parts'"
815        );
816        assert_eq!(details[1]["@type"], errordetails::ERROR_INFO_TYPE);
817        assert_eq!(details[1]["reason"], "INVALID_PARAMS");
818    }
819
820    #[tokio::test]
821    async fn test_subscribe_to_task_not_found() {
822        let app = make_app();
823        let req = Request::builder()
824            .uri("/tasks/nonexistent:subscribe")
825            .method("GET")
826            .body(Body::empty())
827            .unwrap();
828        let resp = app.oneshot(req).await.unwrap();
829        assert!(resp.status().is_client_error());
830    }
831
832    #[tokio::test]
833    async fn test_stream_message() {
834        let app = make_app();
835        let body = serde_json::json!({
836            "message": {
837                "messageId": "m1",
838                "role": "ROLE_USER",
839                "parts": [{"text": "hello"}]
840            }
841        });
842        let req = Request::builder()
843            .uri(REST_STREAM_MESSAGE_PATH)
844            .method("POST")
845            .header("content-type", "application/json")
846            .header("accept", "text/event-stream")
847            .body(Body::from(serde_json::to_string(&body).unwrap()))
848            .unwrap();
849        let resp = app.oneshot(req).await.unwrap();
850        assert_eq!(resp.status(), StatusCode::OK);
851    }
852
853    #[tokio::test]
854    async fn test_get_push_config_not_found() {
855        let app = make_app();
856        let req = Request::builder()
857            .uri("/tasks/t1/pushNotificationConfigs/cfg1")
858            .method("GET")
859            .body(Body::empty())
860            .unwrap();
861        let resp = app.oneshot(req).await.unwrap();
862        // Push not supported → bad request
863        assert!(resp.status().is_client_error());
864    }
865
866    #[tokio::test]
867    async fn test_list_push_configs() {
868        let app = make_push_app();
869        let req = Request::builder()
870            .uri("/tasks/t1/pushNotificationConfigs")
871            .method("GET")
872            .body(Body::empty())
873            .unwrap();
874        let resp = app.oneshot(req).await.unwrap();
875        assert_eq!(resp.status(), StatusCode::OK);
876        let body = resp.into_body().collect().await.unwrap().to_bytes();
877        let payload =
878            serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
879        assert!(payload.configs.is_empty());
880    }
881
882    #[tokio::test]
883    async fn test_delete_push_config() {
884        let app = make_app();
885        let req = Request::builder()
886            .uri("/tasks/t1/pushNotificationConfigs/cfg1")
887            .method("DELETE")
888            .body(Body::empty())
889            .unwrap();
890        let resp = app.oneshot(req).await.unwrap();
891        assert!(resp.status().is_client_error());
892    }
893
894    #[tokio::test]
895    async fn test_delete_push_config_with_store_returns_ok() {
896        let app = make_push_app();
897        let create_body = serde_json::json!({
898            "id": "cfg1",
899            "url": "http://example.com/callback"
900        });
901        let create_req = Request::builder()
902            .uri("/tasks/t1/pushNotificationConfigs")
903            .method("POST")
904            .header("content-type", "application/json")
905            .body(Body::from(serde_json::to_string(&create_body).unwrap()))
906            .unwrap();
907        let create_resp = app.clone().oneshot(create_req).await.unwrap();
908        assert_eq!(create_resp.status(), StatusCode::OK);
909
910        let delete_req = Request::builder()
911            .uri("/tasks/t1/pushNotificationConfigs/cfg1")
912            .method("DELETE")
913            .body(Body::empty())
914            .unwrap();
915        let delete_resp = app.clone().oneshot(delete_req).await.unwrap();
916        assert_eq!(delete_resp.status(), StatusCode::OK);
917
918        let list_req = Request::builder()
919            .uri("/tasks/t1/pushNotificationConfigs")
920            .method("GET")
921            .body(Body::empty())
922            .unwrap();
923        let list_resp = app.oneshot(list_req).await.unwrap();
924        assert_eq!(list_resp.status(), StatusCode::OK);
925        let body = list_resp.into_body().collect().await.unwrap().to_bytes();
926        let payload =
927            serde_json::from_slice::<ListTaskPushNotificationConfigsResponse>(&body).unwrap();
928        assert!(payload.configs.is_empty());
929    }
930
931    #[tokio::test]
932    async fn test_list_tasks_with_query_params() {
933        let app = make_app();
934        let req = Request::builder()
935            .uri("/tasks?contextId=c1&pageSize=5&pageToken=tok&historyLength=3&includeArtifacts=true&statusTimestampAfter=2025-01-01T00:00:00Z")
936            .method("GET")
937            .body(Body::empty())
938            .unwrap();
939        let resp = app.oneshot(req).await.unwrap();
940        assert_eq!(resp.status(), StatusCode::OK);
941    }
942
943    #[tokio::test]
944    async fn test_get_task_with_history_length() {
945        // First create a task, then get it with historyLength
946        let app = make_app();
947        // Create task via send_message
948        let body = serde_json::json!({
949            "message": {
950                "messageId": "m1",
951                "role": "ROLE_USER",
952                "parts": [{"text": "hello"}]
953            }
954        });
955        let req = Request::builder()
956            .uri(REST_SEND_MESSAGE_PATH)
957            .method("POST")
958            .header("content-type", "application/json")
959            .body(Body::from(serde_json::to_string(&body).unwrap()))
960            .unwrap();
961        let resp = app.oneshot(req).await.unwrap();
962        assert_eq!(resp.status(), StatusCode::OK);
963        let resp_body = resp.into_body().collect().await.unwrap().to_bytes();
964        let send_resp: serde_json::Value = serde_json::from_slice(&resp_body).unwrap();
965        let task_id = send_resp["task"]["id"].as_str().unwrap();
966
967        let app = make_app();
968        let req = Request::builder()
969            .uri(format!("/tasks/{}?historyLength=5", task_id))
970            .method("GET")
971            .body(Body::empty())
972            .unwrap();
973        let resp = app.oneshot(req).await.unwrap();
974        // Each make_app() creates a new store, so this will be not found
975        assert!(resp.status() == StatusCode::OK || resp.status() == StatusCode::NOT_FOUND);
976    }
977}