@@ -838,3 +838,101 @@ func falseSchema() *jsonschema.Schema { return &jsonschema.Schema{Not: &jsonsche
838838func nopHandler (context.Context , * ServerSession , * CallToolParamsFor [map [string ]any ]) (* CallToolResult , error ) {
839839 return nil , nil
840840}
841+
842+ func TestKeepAlive (t * testing.T ) {
843+ // TODO: try to use the new synctest package for this test once we upgrade to Go 1.24+.
844+ // synctest would allow us to control time and avoid the time.Sleep calls, making the test
845+ // faster and more deterministic.
846+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
847+ defer cancel ()
848+
849+ ct , st := NewInMemoryTransports ()
850+
851+ serverOpts := & ServerOptions {
852+ KeepAlive : 100 * time .Millisecond ,
853+ }
854+ s := NewServer ("testServer" , "v1.0.0" , serverOpts )
855+ s .AddTools (NewServerTool ("greet" , "say hi" , sayHi ))
856+
857+ ss , err := s .Connect (ctx , st )
858+ if err != nil {
859+ t .Fatal (err )
860+ }
861+ defer ss .Close ()
862+
863+ clientOpts := & ClientOptions {
864+ KeepAlive : 100 * time .Millisecond ,
865+ }
866+ c := NewClient ("testClient" , "v1.0.0" , clientOpts )
867+ cs , err := c .Connect (ctx , ct )
868+ if err != nil {
869+ t .Fatal (err )
870+ }
871+ defer cs .Close ()
872+
873+ // Wait for a few keepalive cycles to ensure pings are working
874+ time .Sleep (300 * time .Millisecond )
875+
876+ // Test that the connection is still alive by making a call
877+ result , err := cs .CallTool (ctx , & CallToolParams {
878+ Name : "greet" ,
879+ Arguments : map [string ]any {"Name" : "user" },
880+ })
881+ if err != nil {
882+ t .Fatalf ("call failed after keepalive: %v" , err )
883+ }
884+ if len (result .Content ) == 0 {
885+ t .Fatal ("expected content in result" )
886+ }
887+ if textContent , ok := result .Content [0 ].(* TextContent ); ! ok || textContent .Text != "hi user" {
888+ t .Fatalf ("unexpected result: %v" , result .Content [0 ])
889+ }
890+ }
891+
892+ func TestKeepAliveFailure (t * testing.T ) {
893+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
894+ defer cancel ()
895+
896+ ct , st := NewInMemoryTransports ()
897+
898+ // Server without keepalive (to test one-sided keepalive)
899+ s := NewServer ("testServer" , "v1.0.0" , nil )
900+ s .AddTools (NewServerTool ("greet" , "say hi" , sayHi ))
901+ ss , err := s .Connect (ctx , st )
902+ if err != nil {
903+ t .Fatal (err )
904+ }
905+
906+ // Client with short keepalive
907+ clientOpts := & ClientOptions {
908+ KeepAlive : 50 * time .Millisecond ,
909+ }
910+ c := NewClient ("testClient" , "v1.0.0" , clientOpts )
911+ cs , err := c .Connect (ctx , ct )
912+ if err != nil {
913+ t .Fatal (err )
914+ }
915+ defer cs .Close ()
916+
917+ // Let the connection establish properly first
918+ time .Sleep (30 * time .Millisecond )
919+
920+ // simulate ping failure
921+ ss .Close ()
922+
923+ // Wait for keepalive to detect the failure and close the client
924+ // check periodically instead of just waiting
925+ deadline := time .Now ().Add (1 * time .Second )
926+ for time .Now ().Before (deadline ) {
927+ _ , err = cs .CallTool (ctx , & CallToolParams {
928+ Name : "greet" ,
929+ Arguments : map [string ]any {"Name" : "user" },
930+ })
931+ if errors .Is (err , ErrConnectionClosed ) {
932+ return // Test passed
933+ }
934+ time .Sleep (25 * time .Millisecond )
935+ }
936+
937+ t .Errorf ("expected connection to be closed by keepalive, but it wasn't. Last error: %v" , err )
938+ }
0 commit comments