@@ -15,8 +15,9 @@ use axum::http::{Request, StatusCode};
1515use contextforge_a2a_runtime:: config:: RuntimeConfig ;
1616use http_body_util:: BodyExt ;
1717use serde_json:: { Value , json} ;
18+ use std:: sync:: Arc ;
1819use tower:: ServiceExt ;
19- use wiremock:: matchers:: { method, path} ;
20+ use wiremock:: matchers:: { body_json , method, path} ;
2021use wiremock:: { Mock , MockServer , ResponseTemplate } ;
2122use test_helpers:: * ;
2223
@@ -1652,6 +1653,12 @@ async fn test_a2a_invoke_create_push_config_routes_to_python() {
16521653
16531654 Mock :: given ( method ( "POST" ) )
16541655 . and ( path_regex ( ".*push/create$" ) )
1656+ . and ( body_json ( json ! ( {
1657+ "a2a_agent_id" : "agent-001" ,
1658+ "task_id" : "task-1" ,
1659+ "webhook_url" : "https://example.com/hook" ,
1660+ "enabled" : true
1661+ } ) ) )
16551662 . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( json ! ( {
16561663 "config_id" : "cfg-1" ,
16571664 "enabled" : true
@@ -1672,7 +1679,12 @@ async fn test_a2a_invoke_create_push_config_routes_to_python() {
16721679 "jsonrpc" : "2.0" ,
16731680 "method" : "CreateTaskPushNotificationConfig" ,
16741681 "id" : 8 ,
1675- "params" : { "task_id" : "task-1" , "webhook_url" : "https://example.com/hook" , "enabled" : true }
1682+ "params" : {
1683+ "a2a_agent_id" : "agent-001" ,
1684+ "task_id" : "task-1" ,
1685+ "webhook_url" : "https://example.com/hook" ,
1686+ "enabled" : true
1687+ }
16761688 } ) ,
16771689 )
16781690 . await ;
@@ -1706,6 +1718,9 @@ async fn test_a2a_invoke_get_push_config_routes_to_python() {
17061718
17071719 Mock :: given ( method ( "POST" ) )
17081720 . and ( path_regex ( ".*push/get$" ) )
1721+ . and ( body_json ( json ! ( {
1722+ "task_id" : "task-1"
1723+ } ) ) )
17091724 . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( json ! ( {
17101725 "config_id" : "cfg-1" ,
17111726 "enabled" : true
@@ -1726,7 +1741,7 @@ async fn test_a2a_invoke_get_push_config_routes_to_python() {
17261741 "jsonrpc" : "2.0" ,
17271742 "method" : "GetTaskPushNotificationConfig" ,
17281743 "id" : 9 ,
1729- "params" : { "config_id " : "cfg -1" }
1744+ "params" : { "task_id " : "task -1" }
17301745 } ) ,
17311746 )
17321747 . await ;
@@ -1760,6 +1775,9 @@ async fn test_a2a_invoke_list_push_configs_routes_to_python() {
17601775
17611776 Mock :: given ( method ( "POST" ) )
17621777 . and ( path_regex ( ".*push/list$" ) )
1778+ . and ( body_json ( json ! ( {
1779+ "task_id" : "task-1"
1780+ } ) ) )
17631781 . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( json ! ( {
17641782 "configs" : [ { "config_id" : "cfg-1" } , { "config_id" : "cfg-2" } ]
17651783 } ) ) )
@@ -1813,6 +1831,9 @@ async fn test_a2a_invoke_delete_push_config_routes_to_python() {
18131831
18141832 Mock :: given ( method ( "POST" ) )
18151833 . and ( path_regex ( ".*push/delete$" ) )
1834+ . and ( body_json ( json ! ( {
1835+ "config_id" : "cfg-1"
1836+ } ) ) )
18161837 . respond_with ( ResponseTemplate :: new ( 200 ) . set_body_json ( json ! ( {
18171838 "deleted" : true
18181839 } ) ) )
@@ -2309,6 +2330,13 @@ async fn test_streaming_method_forwards_sse_stream() {
23092330
23102331 let response = app. oneshot ( request) . await . unwrap ( ) ;
23112332 assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
2333+ assert_eq ! (
2334+ response
2335+ . headers( )
2336+ . get( "content-type" )
2337+ . and_then( |v| v. to_str( ) . ok( ) ) ,
2338+ Some ( "text/event-stream" )
2339+ ) ;
23122340
23132341 let body = String :: from_utf8_lossy (
23142342 & response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ,
@@ -2975,3 +3003,72 @@ async fn test_streaming_method_with_last_event_id_without_redis() {
29753003
29763004 mock_server. verify ( ) . await ;
29773005}
3006+
3007+ #[ tokio:: test]
3008+ async fn test_streaming_method_replays_from_store_when_last_event_id_present ( ) {
3009+ let mock_server = MockServer :: start ( ) . await ;
3010+ setup_auth_mock ( & mock_server, 1 ) . await ;
3011+ setup_authz_mock ( & mock_server, 1 ) . await ;
3012+
3013+ let mut config = default_test_config ( ) ;
3014+ config. backend_base_url = mock_server. uri ( ) ;
3015+ config. auth_secret = Some ( "test-secret" . to_string ( ) ) ;
3016+ let event_store = Arc :: new ( contextforge_a2a_runtime:: event_store:: EventStore :: seeded_for_test (
3017+ vec ! [
3018+ contextforge_a2a_runtime:: event_store:: StoredEvent {
3019+ event_id: "evt-1" . to_string( ) ,
3020+ sequence: 1 ,
3021+ event_type: "unknown" . to_string( ) ,
3022+ payload: r#"{"status":"queued"}"# . to_string( ) ,
3023+ } ,
3024+ contextforge_a2a_runtime:: event_store:: StoredEvent {
3025+ event_id: "evt-2" . to_string( ) ,
3026+ sequence: 2 ,
3027+ event_type: "unknown" . to_string( ) ,
3028+ payload: r#"{"status":"working"}"# . to_string( ) ,
3029+ } ,
3030+ ] ,
3031+ false ,
3032+ ) ) ;
3033+ let app = contextforge_a2a_runtime:: test_support:: build_app_with_event_store (
3034+ config,
3035+ Some ( event_store) ,
3036+ ) ;
3037+
3038+ let request = Request :: builder ( )
3039+ . method ( "POST" )
3040+ . uri ( "/a2a/test-agent/invoke" )
3041+ . header ( "content-type" , "application/json" )
3042+ . header ( "last-event-id" , "task-123:0" )
3043+ . body ( Body :: from (
3044+ serde_json:: to_vec ( & json ! ( {
3045+ "jsonrpc" : "2.0" ,
3046+ "method" : "SendStreamingMessage" ,
3047+ "id" : 1 ,
3048+ "params" : { "id" : "task-123" , "message" : { "role" : "ROLE_USER" , "parts" : [ { "text" : "hello" } ] } }
3049+ } ) )
3050+ . unwrap ( ) ,
3051+ ) )
3052+ . unwrap ( ) ;
3053+
3054+ let response = app. oneshot ( request) . await . unwrap ( ) ;
3055+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
3056+ assert_eq ! (
3057+ response
3058+ . headers( )
3059+ . get( "content-type" )
3060+ . and_then( |v| v. to_str( ) . ok( ) ) ,
3061+ Some ( "text/event-stream" )
3062+ ) ;
3063+
3064+ let body = String :: from_utf8_lossy (
3065+ & response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ,
3066+ )
3067+ . to_string ( ) ;
3068+ assert ! ( body. contains( "id: evt-1:1" ) , "expected first replayed event, body: {body}" ) ;
3069+ assert ! ( body. contains( "id: evt-2:2" ) , "expected second replayed event, body: {body}" ) ;
3070+ assert ! ( body. contains( "data: {\" status\" :\" queued\" }" ) , "expected queued payload, body: {body}" ) ;
3071+ assert ! ( body. contains( "data: {\" status\" :\" working\" }" ) , "expected working payload, body: {body}" ) ;
3072+
3073+ mock_server. verify ( ) . await ;
3074+ }
0 commit comments