This is an automated email from the ASF dual-hosted git repository.

alexstocks pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 6bae0d832 test(triple): enhance unit tests for protocol/triple package 
(#3133)
6bae0d832 is described below

commit 6bae0d83251637d092ebc492e8163995a8f2c1e6
Author: CAICAII <[email protected]>
AuthorDate: Mon Dec 22 07:43:36 2025 +0800

    test(triple): enhance unit tests for protocol/triple package (#3133)
---
 protocol/triple/client_test.go         | 560 +++++++++++++++++++++++++++++++++
 protocol/triple/server_test.go         | 343 +++++++++++++++++++-
 protocol/triple/triple_invoker_test.go | 456 +++++++++++++++++++++++++++
 protocol/triple/triple_test.go         |  69 ++++
 4 files changed, 1427 insertions(+), 1 deletion(-)

diff --git a/protocol/triple/client_test.go b/protocol/triple/client_test.go
index 8f39c4458..4f40e6f74 100644
--- a/protocol/triple/client_test.go
+++ b/protocol/triple/client_test.go
@@ -18,6 +18,7 @@
 package triple
 
 import (
+       "context"
        "net/http"
        "testing"
        "time"
@@ -31,6 +32,7 @@ import (
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/global"
+       tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
 )
 
 func TestClientManager_HTTP2AndHTTP3(t *testing.T) {
@@ -94,3 +96,561 @@ func TestDualTransport(t *testing.T) {
        })
        assert.True(t, ok, "transport should implement http.RoundTripper")
 }
+
+func TestClientManager_GetClient(t *testing.T) {
+       tests := []struct {
+               desc      string
+               cm        *clientManager
+               method    string
+               expectErr bool
+       }{
+               {
+                       desc: "method exists",
+                       cm: &clientManager{
+                               triClients: map[string]*tri.Client{
+                                       "TestMethod": 
tri.NewClient(&http.Client{}, "http://localhost:8080/test";),
+                               },
+                       },
+                       method:    "TestMethod",
+                       expectErr: false,
+               },
+               {
+                       desc: "method not exists",
+                       cm: &clientManager{
+                               triClients: map[string]*tri.Client{
+                                       "TestMethod": 
tri.NewClient(&http.Client{}, "http://localhost:8080/test";),
+                               },
+                       },
+                       method:    "NonExistMethod",
+                       expectErr: true,
+               },
+               {
+                       desc: "empty triClients",
+                       cm: &clientManager{
+                               triClients: map[string]*tri.Client{},
+                       },
+                       method:    "AnyMethod",
+                       expectErr: true,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       client, err := test.cm.getClient(test.method)
+                       if test.expectErr {
+                               assert.NotNil(t, err)
+                               assert.Nil(t, client)
+                               assert.Contains(t, err.Error(), "missing triple 
client")
+                       } else {
+                               assert.Nil(t, err)
+                               assert.NotNil(t, client)
+                       }
+               })
+       }
+}
+
+func TestClientManager_Close(t *testing.T) {
+       cm := &clientManager{
+               isIDL: true,
+               triClients: map[string]*tri.Client{
+                       "Method1": tri.NewClient(&http.Client{}, 
"http://localhost:8080/test1";),
+                       "Method2": tri.NewClient(&http.Client{}, 
"http://localhost:8080/test2";),
+               },
+       }
+
+       err := cm.close()
+       assert.Nil(t, err)
+}
+
+func TestClientManager_CallMethods_MissingClient(t *testing.T) {
+       cm := &clientManager{
+               triClients: map[string]*tri.Client{},
+       }
+       ctx := context.Background()
+
+       t.Run("callUnary missing client", func(t *testing.T) {
+               err := cm.callUnary(ctx, "NonExist", nil, nil)
+               assert.NotNil(t, err)
+               assert.Contains(t, err.Error(), "missing triple client")
+       })
+
+       t.Run("callClientStream missing client", func(t *testing.T) {
+               stream, err := cm.callClientStream(ctx, "NonExist")
+               assert.NotNil(t, err)
+               assert.Nil(t, stream)
+               assert.Contains(t, err.Error(), "missing triple client")
+       })
+
+       t.Run("callServerStream missing client", func(t *testing.T) {
+               stream, err := cm.callServerStream(ctx, "NonExist", nil)
+               assert.NotNil(t, err)
+               assert.Nil(t, stream)
+               assert.Contains(t, err.Error(), "missing triple client")
+       })
+
+       t.Run("callBidiStream missing client", func(t *testing.T) {
+               stream, err := cm.callBidiStream(ctx, "NonExist")
+               assert.NotNil(t, err)
+               assert.Nil(t, stream)
+               assert.Contains(t, err.Error(), "missing triple client")
+       })
+}
+
+func Test_genKeepAliveOptions(t *testing.T) {
+       defaultInterval, _ := 
time.ParseDuration(constant.DefaultKeepAliveInterval)
+       defaultTimeout, _ := 
time.ParseDuration(constant.DefaultKeepAliveTimeout)
+
+       tests := []struct {
+               desc           string
+               url            *common.URL
+               tripleConf     *global.TripleConfig
+               expectOptsLen  int
+               expectInterval time.Duration
+               expectTimeout  time.Duration
+               expectErr      bool
+       }{
+               {
+                       desc:           "nil triple config",
+                       url:            common.NewURLWithOptions(),
+                       tripleConf:     nil,
+                       expectOptsLen:  2, // readMaxBytes, sendMaxBytes
+                       expectInterval: defaultInterval,
+                       expectTimeout:  defaultTimeout,
+                       expectErr:      false,
+               },
+               {
+                       desc: "url with max msg size",
+                       url: common.NewURLWithOptions(
+                               
common.WithParamsValue(constant.MaxCallRecvMsgSize, "10MB"),
+                               
common.WithParamsValue(constant.MaxCallSendMsgSize, "10MB"),
+                       ),
+                       tripleConf:     nil,
+                       expectOptsLen:  2,
+                       expectInterval: defaultInterval,
+                       expectTimeout:  defaultTimeout,
+                       expectErr:      false,
+               },
+               {
+                       desc: "url with keepalive params",
+                       url: common.NewURLWithOptions(
+                               
common.WithParamsValue(constant.KeepAliveInterval, "60s"),
+                               
common.WithParamsValue(constant.KeepAliveTimeout, "20s"),
+                       ),
+                       tripleConf:     nil,
+                       expectOptsLen:  2,
+                       expectInterval: 60 * time.Second,
+                       expectTimeout:  20 * time.Second,
+                       expectErr:      false,
+               },
+               {
+                       desc: "triple config with keepalive",
+                       url:  common.NewURLWithOptions(),
+                       tripleConf: &global.TripleConfig{
+                               KeepAliveInterval: "45s",
+                               KeepAliveTimeout:  "15s",
+                       },
+                       expectOptsLen:  2,
+                       expectInterval: 45 * time.Second,
+                       expectTimeout:  15 * time.Second,
+                       expectErr:      false,
+               },
+               {
+                       desc: "triple config with invalid interval",
+                       url:  common.NewURLWithOptions(),
+                       tripleConf: &global.TripleConfig{
+                               KeepAliveInterval: "invalid",
+                       },
+                       expectErr: true,
+               },
+               {
+                       desc: "triple config with invalid timeout",
+                       url:  common.NewURLWithOptions(),
+                       tripleConf: &global.TripleConfig{
+                               KeepAliveTimeout: "invalid",
+                       },
+                       expectErr: true,
+               },
+               {
+                       desc:           "empty triple config",
+                       url:            common.NewURLWithOptions(),
+                       tripleConf:     &global.TripleConfig{},
+                       expectOptsLen:  2,
+                       expectInterval: defaultInterval,
+                       expectTimeout:  defaultTimeout,
+                       expectErr:      false,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       opts, interval, timeout, err := 
genKeepAliveOptions(test.url, test.tripleConf)
+                       if test.expectErr {
+                               assert.NotNil(t, err)
+                       } else {
+                               assert.Nil(t, err)
+                               assert.Equal(t, test.expectOptsLen, len(opts))
+                               assert.Equal(t, test.expectInterval, interval)
+                               assert.Equal(t, test.expectTimeout, timeout)
+                       }
+               })
+       }
+}
+
+func Test_newClientManager_Serialization(t *testing.T) {
+       tests := []struct {
+               desc          string
+               serialization string
+               expectIDL     bool
+               expectPanic   bool
+       }{
+               {
+                       desc:          "protobuf serialization",
+                       serialization: constant.ProtobufSerialization,
+                       expectIDL:     true,
+                       expectPanic:   false,
+               },
+               {
+                       desc:          "json serialization",
+                       serialization: constant.JSONSerialization,
+                       expectIDL:     true,
+                       expectPanic:   false,
+               },
+               {
+                       desc:          "hessian2 serialization",
+                       serialization: constant.Hessian2Serialization,
+                       expectIDL:     false,
+                       expectPanic:   false,
+               },
+               {
+                       desc:          "msgpack serialization",
+                       serialization: constant.MsgpackSerialization,
+                       expectIDL:     false,
+                       expectPanic:   false,
+               },
+               {
+                       desc:          "unsupported serialization",
+                       serialization: "unsupported",
+                       expectPanic:   true,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       url := common.NewURLWithOptions(
+                               common.WithLocation("localhost:20000"),
+                               common.WithPath("com.example.TestService"),
+                               common.WithMethods([]string{"TestMethod"}),
+                               
common.WithParamsValue(constant.SerializationKey, test.serialization),
+                       )
+
+                       if test.expectPanic {
+                               assert.Panics(t, func() {
+                                       _, _ = newClientManager(url)
+                               })
+                       } else {
+                               cm, err := newClientManager(url)
+                               assert.Nil(t, err)
+                               assert.NotNil(t, cm)
+                               assert.Equal(t, test.expectIDL, cm.isIDL)
+                       }
+               })
+       }
+}
+
+func Test_newClientManager_NoMethods(t *testing.T) {
+       // Test when url has no methods and no RpcServiceKey attribute
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+       )
+
+       cm, err := newClientManager(url)
+       assert.NotNil(t, err)
+       assert.Nil(t, cm)
+       assert.Contains(t, err.Error(), "can't get methods")
+}
+
+func Test_newClientManager_WithMethods(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"Method1", "Method2", "Method3"}),
+       )
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+       assert.Equal(t, 3, len(cm.triClients))
+       assert.Contains(t, cm.triClients, "Method1")
+       assert.Contains(t, cm.triClients, "Method2")
+       assert.Contains(t, cm.triClients, "Method3")
+}
+
+func Test_newClientManager_WithGroupAndVersion(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+               common.WithParamsValue(constant.GroupKey, "testGroup"),
+               common.WithParamsValue(constant.VersionKey, "1.0.0"),
+       )
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_WithTimeout(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+               common.WithParamsValue(constant.TimeoutKey, "5s"),
+       )
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_InvalidTLSConfig(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+       // Set invalid TLS config type
+       url.SetAttribute(constant.TLSConfigKey, "invalid-type")
+
+       cm, err := newClientManager(url)
+       assert.NotNil(t, err)
+       assert.Nil(t, cm)
+       assert.Contains(t, err.Error(), "TLSConfig configuration failed")
+}
+
+func Test_newClientManager_HTTP3WithoutTLS(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+       // Enable HTTP/3 without TLS config
+       tripleConfig := &global.TripleConfig{
+               Http3: &global.Http3Config{
+                       Enable: true,
+               },
+       }
+       url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+       cm, err := newClientManager(url)
+       assert.NotNil(t, err)
+       assert.Nil(t, cm)
+       assert.Contains(t, err.Error(), "must have TLS config")
+}
+
+// mockService is a mock service for testing reflection-based client creation
+type mockService struct{}
+
+func (m *mockService) Reference() string {
+       return "mockService"
+}
+
+func (m *mockService) TestMethod1(ctx context.Context, req string) (string, 
error) {
+       return req, nil
+}
+
+func (m *mockService) TestMethod2(ctx context.Context, req int) (int, error) {
+       return req, nil
+}
+
+func Test_newClientManager_WithRpcService(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               // No methods specified, will use reflection
+       )
+       url.SetAttribute(constant.RpcServiceKey, &mockService{})
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+       // Should have methods from mockService (Reference, TestMethod1, 
TestMethod2)
+       assert.True(t, len(cm.triClients) >= 2)
+}
+
+func TestDualTransport_Structure(t *testing.T) {
+       keepAliveInterval := 30 * time.Second
+       keepAliveTimeout := 5 * time.Second
+
+       transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+       assert.NotNil(t, transport)
+
+       dt, ok := transport.(*dualTransport)
+       assert.True(t, ok)
+       assert.NotNil(t, dt.http2Transport)
+       assert.NotNil(t, dt.http3Transport)
+       assert.NotNil(t, dt.altSvcCache)
+}
+
+func Test_newClientManager_HTTP2WithTLS(t *testing.T) {
+       // This test requires valid TLS config files
+       // Skip if files don't exist
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+
+       // Set a valid TLS config structure but with non-existent files
+       // This will test the TLS config parsing path
+       tlsConfig := &global.TLSConfig{
+               CACertFile:  "non-existent-ca.crt",
+               TLSCertFile: "non-existent-server.crt",
+               TLSKeyFile:  "non-existent-server.key",
+       }
+       url.SetAttribute(constant.TLSConfigKey, tlsConfig)
+
+       // Should fail due to missing cert files, but tests the TLS path
+       cm, err := newClientManager(url)
+       // Either succeeds (if TLS validation is lenient) or fails with TLS 
error
+       if err != nil {
+               // Expected - TLS files don't exist
+               t.Logf("Expected TLS error: %v", err)
+       } else {
+               assert.NotNil(t, cm)
+       }
+}
+
+func Test_newClientManager_URLPrefixHandling(t *testing.T) {
+       tests := []struct {
+               desc     string
+               location string
+       }{
+               {
+                       desc:     "location without prefix",
+                       location: "localhost:20000",
+               },
+               {
+                       desc:     "location with http prefix",
+                       location: "http://localhost:20000";,
+               },
+               {
+                       desc:     "location with https prefix",
+                       location: "https://localhost:20000";,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       url := common.NewURLWithOptions(
+                               common.WithLocation(test.location),
+                               common.WithPath("com.example.TestService"),
+                               common.WithMethods([]string{"TestMethod"}),
+                       )
+
+                       cm, err := newClientManager(url)
+                       assert.Nil(t, err)
+                       assert.NotNil(t, cm)
+                       assert.Equal(t, 1, len(cm.triClients))
+               })
+       }
+}
+
+func Test_newClientManager_KeepAliveError(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+
+       // Set triple config with invalid keepalive that will cause error
+       tripleConfig := &global.TripleConfig{
+               KeepAliveInterval: "invalid-duration",
+       }
+       url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+       cm, err := newClientManager(url)
+       assert.NotNil(t, err)
+       assert.Nil(t, cm)
+}
+
+func Test_newClientManager_DefaultProtocol(t *testing.T) {
+       // Test default HTTP/2 protocol selection
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_EmptyTripleConfig(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+
+       // Set empty triple config (Http3 is nil)
+       tripleConfig := &global.TripleConfig{}
+       url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_Http3Disabled(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+
+       // Set triple config with Http3 disabled
+       tripleConfig := &global.TripleConfig{
+               Http3: &global.Http3Config{
+                       Enable: false,
+               },
+       }
+       url.SetAttribute(constant.TripleConfigKey, tripleConfig)
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+}
+
+func Test_newClientManager_MultipleMethods(t *testing.T) {
+       methods := []string{"Method1", "Method2", "Method3", "Method4", 
"Method5"}
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithMethods(methods),
+       )
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+       assert.Equal(t, len(methods), len(cm.triClients))
+
+       for _, method := range methods {
+               _, exists := cm.triClients[method]
+               assert.True(t, exists, "method %s should exist", method)
+       }
+}
+
+func Test_newClientManager_InterfaceName(t *testing.T) {
+       url := common.NewURLWithOptions(
+               common.WithLocation("localhost:20000"),
+               common.WithPath("com.example.TestService"),
+               common.WithInterface("com.example.ITestService"),
+               common.WithMethods([]string{"TestMethod"}),
+       )
+
+       cm, err := newClientManager(url)
+       assert.Nil(t, err)
+       assert.NotNil(t, cm)
+}
diff --git a/protocol/triple/server_test.go b/protocol/triple/server_test.go
index c6b705141..d6129fea6 100644
--- a/protocol/triple/server_test.go
+++ b/protocol/triple/server_test.go
@@ -18,15 +18,21 @@
 package triple
 
 import (
+       "context"
+       "fmt"
        "net/http"
+       "sync"
        "testing"
 )
 
 import (
        "github.com/stretchr/testify/assert"
+
+       "google.golang.org/grpc"
 )
 
 import (
+       "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/global"
 )
@@ -135,7 +141,7 @@ func TestServer_ProtocolSelection(t *testing.T) {
 
                        // Extract the protocol selection logic for testing
                        var callProtocol string
-                       if tripleConfig != nil && tripleConfig.Http3 != nil && 
tripleConfig.Http3.Enable {
+                       if tripleConfig.Http3 != nil && 
tripleConfig.Http3.Enable {
                                callProtocol = constant.CallHTTP2AndHTTP3
                        } else {
                                callProtocol = constant.CallHTTP2
@@ -145,3 +151,338 @@ func TestServer_ProtocolSelection(t *testing.T) {
                })
        }
 }
+
+func TestNewServer(t *testing.T) {
+       tests := []struct {
+               desc string
+               cfg  *global.TripleConfig
+       }{
+               {
+                       desc: "nil config",
+                       cfg:  nil,
+               },
+               {
+                       desc: "empty config",
+                       cfg:  &global.TripleConfig{},
+               },
+               {
+                       desc: "config with http3",
+                       cfg: &global.TripleConfig{
+                               Http3: &global.Http3Config{
+                                       Enable: true,
+                               },
+                       },
+               },
+               {
+                       desc: "config with keepalive",
+                       cfg: &global.TripleConfig{
+                               KeepAliveInterval: "30s",
+                               KeepAliveTimeout:  "10s",
+                       },
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       server := NewServer(test.cfg)
+                       assert.NotNil(t, server)
+                       assert.Equal(t, test.cfg, server.cfg)
+                       assert.NotNil(t, server.services)
+                       assert.Empty(t, server.services)
+               })
+       }
+}
+
+func TestServer_GetServiceInfo(t *testing.T) {
+       t.Run("empty services", func(t *testing.T) {
+               server := NewServer(nil)
+               info := server.GetServiceInfo()
+               assert.NotNil(t, info)
+               assert.Empty(t, info)
+       })
+
+       t.Run("with services", func(t *testing.T) {
+               server := NewServer(nil)
+               // manually add service info for testing
+               server.mu.Lock()
+               server.services["test.Service"] = grpc.ServiceInfo{
+                       Methods: []grpc.MethodInfo{
+                               {Name: "Method1", IsClientStream: false, 
IsServerStream: false},
+                       },
+               }
+               server.mu.Unlock()
+
+               info := server.GetServiceInfo()
+               assert.NotNil(t, info)
+               assert.Equal(t, 1, len(info))
+               assert.Contains(t, info, "test.Service")
+       })
+
+       t.Run("returns copy not reference", func(t *testing.T) {
+               server := NewServer(nil)
+               server.mu.Lock()
+               server.services["test.Service"] = grpc.ServiceInfo{}
+               server.mu.Unlock()
+
+               info1 := server.GetServiceInfo()
+               info2 := server.GetServiceInfo()
+
+               // modify info1 should not affect info2
+               delete(info1, "test.Service")
+               assert.Contains(t, info2, "test.Service")
+       })
+}
+
+func TestServer_SaveServiceInfo(t *testing.T) {
+       tests := []struct {
+               desc          string
+               interfaceName string
+               info          *common.ServiceInfo
+               expect        func(t *testing.T, server *Server)
+       }{
+               {
+                       desc:          "unary method",
+                       interfaceName: "test.UnaryService",
+                       info: &common.ServiceInfo{
+                               Methods: []common.MethodInfo{
+                                       {Name: "UnaryMethod", Type: 
constant.CallUnary},
+                               },
+                       },
+                       expect: func(t *testing.T, server *Server) {
+                               info := server.GetServiceInfo()
+                               assert.Contains(t, info, "test.UnaryService")
+                               assert.Equal(t, 1, 
len(info["test.UnaryService"].Methods))
+                               assert.Equal(t, "UnaryMethod", 
info["test.UnaryService"].Methods[0].Name)
+                               assert.False(t, 
info["test.UnaryService"].Methods[0].IsClientStream)
+                               assert.False(t, 
info["test.UnaryService"].Methods[0].IsServerStream)
+                       },
+               },
+               {
+                       desc:          "client stream method",
+                       interfaceName: "test.ClientStreamService",
+                       info: &common.ServiceInfo{
+                               Methods: []common.MethodInfo{
+                                       {Name: "ClientStreamMethod", Type: 
constant.CallClientStream},
+                               },
+                       },
+                       expect: func(t *testing.T, server *Server) {
+                               info := server.GetServiceInfo()
+                               assert.Contains(t, info, 
"test.ClientStreamService")
+                               assert.True(t, 
info["test.ClientStreamService"].Methods[0].IsClientStream)
+                               assert.False(t, 
info["test.ClientStreamService"].Methods[0].IsServerStream)
+                       },
+               },
+               {
+                       desc:          "server stream method",
+                       interfaceName: "test.ServerStreamService",
+                       info: &common.ServiceInfo{
+                               Methods: []common.MethodInfo{
+                                       {Name: "ServerStreamMethod", Type: 
constant.CallServerStream},
+                               },
+                       },
+                       expect: func(t *testing.T, server *Server) {
+                               info := server.GetServiceInfo()
+                               assert.Contains(t, info, 
"test.ServerStreamService")
+                               assert.False(t, 
info["test.ServerStreamService"].Methods[0].IsClientStream)
+                               assert.True(t, 
info["test.ServerStreamService"].Methods[0].IsServerStream)
+                       },
+               },
+               {
+                       desc:          "bidi stream method",
+                       interfaceName: "test.BidiStreamService",
+                       info: &common.ServiceInfo{
+                               Methods: []common.MethodInfo{
+                                       {Name: "BidiStreamMethod", Type: 
constant.CallBidiStream},
+                               },
+                       },
+                       expect: func(t *testing.T, server *Server) {
+                               info := server.GetServiceInfo()
+                               assert.Contains(t, info, 
"test.BidiStreamService")
+                               assert.True(t, 
info["test.BidiStreamService"].Methods[0].IsClientStream)
+                               assert.True(t, 
info["test.BidiStreamService"].Methods[0].IsServerStream)
+                       },
+               },
+               {
+                       desc:          "multiple methods",
+                       interfaceName: "test.MultiMethodService",
+                       info: &common.ServiceInfo{
+                               Methods: []common.MethodInfo{
+                                       {Name: "Method1", Type: 
constant.CallUnary},
+                                       {Name: "Method2", Type: 
constant.CallClientStream},
+                                       {Name: "Method3", Type: 
constant.CallServerStream},
+                                       {Name: "Method4", Type: 
constant.CallBidiStream},
+                               },
+                       },
+                       expect: func(t *testing.T, server *Server) {
+                               info := server.GetServiceInfo()
+                               assert.Contains(t, info, 
"test.MultiMethodService")
+                               assert.Equal(t, 4, 
len(info["test.MultiMethodService"].Methods))
+                       },
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       server := NewServer(nil)
+                       server.saveServiceInfo(test.interfaceName, test.info)
+                       test.expect(t, server)
+               })
+       }
+}
+
+func TestServer_SaveServiceInfo_Concurrent(t *testing.T) {
+       server := NewServer(nil)
+       var wg sync.WaitGroup
+       concurrency := 10
+
+       for i := 0; i < concurrency; i++ {
+               wg.Add(1)
+               go func(idx int) {
+                       defer wg.Done()
+                       info := &common.ServiceInfo{
+                               Methods: []common.MethodInfo{
+                                       {Name: "Method", Type: 
constant.CallUnary},
+                               },
+                       }
+                       server.saveServiceInfo(fmt.Sprintf("test.Service%d", 
idx), info)
+               }(i)
+       }
+
+       wg.Wait()
+       assert.Equal(t, concurrency, len(server.GetServiceInfo()))
+}
+
+func Test_getHanOpts(t *testing.T) {
+       tests := []struct {
+               desc       string
+               url        *common.URL
+               tripleConf *global.TripleConfig
+               expectLen  int
+       }{
+               {
+                       desc:       "basic url without triple config",
+                       url:        common.NewURLWithOptions(),
+                       tripleConf: nil,
+                       expectLen:  4, // group, version, readMaxBytes, 
sendMaxBytes
+               },
+               {
+                       desc: "url with group and version",
+                       url: common.NewURLWithOptions(
+                               common.WithParamsValue(constant.GroupKey, 
"testGroup"),
+                               common.WithParamsValue(constant.VersionKey, 
"1.0.0"),
+                       ),
+                       tripleConf: nil,
+                       expectLen:  4,
+               },
+               {
+                       desc: "url with max msg size",
+                       url: common.NewURLWithOptions(
+                               
common.WithParamsValue(constant.MaxServerRecvMsgSize, "10MB"),
+                               
common.WithParamsValue(constant.MaxServerSendMsgSize, "10MB"),
+                       ),
+                       tripleConf: nil,
+                       expectLen:  4,
+               },
+               {
+                       desc: "with triple config max msg size",
+                       url:  common.NewURLWithOptions(),
+                       tripleConf: &global.TripleConfig{
+                               MaxServerRecvMsgSize: "20MB",
+                               MaxServerSendMsgSize: "20MB",
+                       },
+                       expectLen: 6, // base 4 + 2 from tripleConf
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       opts := getHanOpts(test.url, test.tripleConf)
+                       assert.Equal(t, test.expectLen, len(opts))
+               })
+       }
+}
+
+// mockRPCService is a mock service for testing createServiceInfoWithReflection
+type mockRPCService struct{}
+
+func (m *mockRPCService) Reference() string {
+       return "mockRPCService"
+}
+
+func (m *mockRPCService) TestMethod(ctx context.Context, req string) (string, 
error) {
+       return req, nil
+}
+
+func (m *mockRPCService) TestMethodWithMultipleArgs(ctx context.Context, arg1 
string, arg2 int) (string, error) {
+       return arg1, nil
+}
+
+func Test_createServiceInfoWithReflection(t *testing.T) {
+       t.Run("basic service", func(t *testing.T) {
+               svc := &mockRPCService{}
+               info := createServiceInfoWithReflection(svc)
+
+               assert.NotNil(t, info)
+               assert.NotEmpty(t, info.Methods)
+
+               // should have TestMethod, TestMethodWithMultipleArgs, and 
$invoke
+               methodNames := make([]string, 0)
+               for _, m := range info.Methods {
+                       methodNames = append(methodNames, m.Name)
+               }
+               assert.Contains(t, methodNames, "TestMethod")
+               assert.Contains(t, methodNames, "TestMethodWithMultipleArgs")
+               assert.Contains(t, methodNames, "$invoke") // generic call 
method
+       })
+
+       t.Run("method type is CallUnary", func(t *testing.T) {
+               svc := &mockRPCService{}
+               info := createServiceInfoWithReflection(svc)
+
+               for _, m := range info.Methods {
+                       assert.Equal(t, constant.CallUnary, m.Type)
+               }
+       })
+
+       t.Run("ReqInitFunc returns correct params", func(t *testing.T) {
+               svc := &mockRPCService{}
+               info := createServiceInfoWithReflection(svc)
+
+               for _, m := range info.Methods {
+                       if m.Name == "TestMethod" {
+                               params := m.ReqInitFunc()
+                               assert.NotNil(t, params)
+                               paramsSlice, ok := params.([]any)
+                               assert.True(t, ok)
+                               assert.Equal(t, 1, len(paramsSlice)) // only 
req param (ctx is excluded)
+                       }
+                       if m.Name == "TestMethodWithMultipleArgs" {
+                               params := m.ReqInitFunc()
+                               paramsSlice, ok := params.([]any)
+                               assert.True(t, ok)
+                               assert.Equal(t, 2, len(paramsSlice)) // arg1 
and arg2
+                       }
+               }
+       })
+
+       t.Run("generic invoke method", func(t *testing.T) {
+               svc := &mockRPCService{}
+               info := createServiceInfoWithReflection(svc)
+
+               var invokeMethod *common.MethodInfo
+               for i := range info.Methods {
+                       if info.Methods[i].Name == "$invoke" {
+                               invokeMethod = &info.Methods[i]
+                               break
+                       }
+               }
+
+               assert.NotNil(t, invokeMethod)
+               assert.Equal(t, constant.CallUnary, invokeMethod.Type)
+
+               params := invokeMethod.ReqInitFunc()
+               paramsSlice, ok := params.([]any)
+               assert.True(t, ok)
+               assert.Equal(t, 3, len(paramsSlice)) // methodName, argv types, 
argv
+       })
+}
diff --git a/protocol/triple/triple_invoker_test.go 
b/protocol/triple/triple_invoker_test.go
index 09788c450..9f8575aaa 100644
--- a/protocol/triple/triple_invoker_test.go
+++ b/protocol/triple/triple_invoker_test.go
@@ -20,6 +20,7 @@ package triple
 import (
        "context"
        "net/http"
+       "sync"
        "testing"
 )
 
@@ -54,6 +55,7 @@ func Test_parseInvocation(t *testing.T) {
                        },
                        expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
                                assert.NotNil(t, err)
+                               assert.Contains(t, err.Error(), "miss CallType")
                        },
                },
                {
@@ -69,6 +71,7 @@ func Test_parseInvocation(t *testing.T) {
                        },
                        expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
                                assert.NotNil(t, err)
+                               assert.Contains(t, err.Error(), "CallType 
should be string")
                        },
                },
                {
@@ -84,6 +87,87 @@ func Test_parseInvocation(t *testing.T) {
                        },
                        expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
                                assert.NotNil(t, err)
+                               assert.Contains(t, err.Error(), "miss 
MethodName")
+                       },
+               },
+               {
+                       desc: "success with CallUnary",
+                       ctx: func() context.Context {
+                               return context.Background()
+                       },
+                       url: common.NewURLWithOptions(),
+                       invo: func() base.Invocation {
+                               iv := invocation.NewRPCInvocationWithOptions(
+                                       invocation.WithMethodName("TestMethod"),
+                                       
invocation.WithParameterRawValues([]any{"req", "resp"}),
+                               )
+                               iv.SetAttribute(constant.CallTypeKey, 
constant.CallUnary)
+                               return iv
+                       },
+                       expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
+                               assert.Nil(t, err)
+                               assert.Equal(t, constant.CallUnary, callType)
+                               assert.Equal(t, "TestMethod", methodName)
+                               assert.Equal(t, 2, len(inRaw))
+                       },
+               },
+               {
+                       desc: "success with CallClientStream",
+                       ctx: func() context.Context {
+                               return context.Background()
+                       },
+                       url: common.NewURLWithOptions(),
+                       invo: func() base.Invocation {
+                               iv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithMethodName("StreamMethod"),
+                               )
+                               iv.SetAttribute(constant.CallTypeKey, 
constant.CallClientStream)
+                               return iv
+                       },
+                       expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
+                               assert.Nil(t, err)
+                               assert.Equal(t, constant.CallClientStream, 
callType)
+                               assert.Equal(t, "StreamMethod", methodName)
+                       },
+               },
+               {
+                       desc: "success with CallServerStream",
+                       ctx: func() context.Context {
+                               return context.Background()
+                       },
+                       url: common.NewURLWithOptions(),
+                       invo: func() base.Invocation {
+                               iv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithMethodName("ServerStreamMethod"),
+                                       
invocation.WithParameterRawValues([]any{"req"}),
+                               )
+                               iv.SetAttribute(constant.CallTypeKey, 
constant.CallServerStream)
+                               return iv
+                       },
+                       expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
+                               assert.Nil(t, err)
+                               assert.Equal(t, constant.CallServerStream, 
callType)
+                               assert.Equal(t, "ServerStreamMethod", 
methodName)
+                               assert.Equal(t, 1, len(inRaw))
+                       },
+               },
+               {
+                       desc: "success with CallBidiStream",
+                       ctx: func() context.Context {
+                               return context.Background()
+                       },
+                       url: common.NewURLWithOptions(),
+                       invo: func() base.Invocation {
+                               iv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithMethodName("BidiStreamMethod"),
+                               )
+                               iv.SetAttribute(constant.CallTypeKey, 
constant.CallBidiStream)
+                               return iv
+                       },
+                       expect: func(t *testing.T, callType string, inRaw 
[]any, methodName string, err error) {
+                               assert.Nil(t, err)
+                               assert.Equal(t, constant.CallBidiStream, 
callType)
+                               assert.Equal(t, "BidiStreamMethod", methodName)
                        },
                },
        }
@@ -157,6 +241,52 @@ func Test_parseAttachments(t *testing.T) {
                        },
                        expect: func(t *testing.T, ctx context.Context, err 
error) {
                                assert.NotNil(t, err)
+                               assert.Contains(t, err.Error(), "invalid")
+                       },
+               },
+               {
+                       desc: "url has timeout key",
+                       ctx: func() context.Context {
+                               return context.Background()
+                       },
+                       url: common.NewURLWithOptions(
+                               common.WithParamsValue(constant.TimeoutKey, 
"3000"),
+                       ),
+                       invo: func() base.Invocation {
+                               return invocation.NewRPCInvocationWithOptions()
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.Nil(t, err)
+                               header := 
http.Header(tri.ExtractFromOutgoingContext(ctx))
+                               assert.NotNil(t, header)
+                               assert.Equal(t, "3000", 
header.Get(constant.TimeoutKey))
+                       },
+               },
+               {
+                       desc: "empty context attachments",
+                       ctx: func() context.Context {
+                               return context.Background()
+                       },
+                       url: common.NewURLWithOptions(),
+                       invo: func() base.Invocation {
+                               return invocation.NewRPCInvocationWithOptions()
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.Nil(t, err)
+                       },
+               },
+               {
+                       desc: "context with non-map attachment value",
+                       ctx: func() context.Context {
+                               return context.WithValue(context.Background(), 
constant.AttachmentKey, "not-a-map")
+                       },
+                       url: common.NewURLWithOptions(),
+                       invo: func() base.Invocation {
+                               return invocation.NewRPCInvocationWithOptions()
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               // non-map attachment should be ignored
+                               assert.Nil(t, err)
                        },
                },
        }
@@ -171,3 +301,329 @@ func Test_parseAttachments(t *testing.T) {
                })
        }
 }
+
+// newTestTripleInvoker creates a TripleInvoker for testing without network 
connection
+func newTestTripleInvoker(url *common.URL, cm *clientManager) *TripleInvoker {
+       return &TripleInvoker{
+               BaseInvoker:   *base.NewBaseInvoker(url),
+               quitOnce:      sync.Once{},
+               clientGuard:   &sync.RWMutex{},
+               clientManager: cm,
+       }
+}
+
+func TestTripleInvoker_SetGetClientManager(t *testing.T) {
+       url := common.NewURLWithOptions()
+       ti := newTestTripleInvoker(url, nil)
+
+       // initially nil
+       assert.Nil(t, ti.getClientManager())
+
+       // set clientManager
+       cm := &clientManager{
+               isIDL:      true,
+               triClients: make(map[string]*tri.Client),
+       }
+       ti.setClientManager(cm)
+       assert.Equal(t, cm, ti.getClientManager())
+
+       // set to nil
+       ti.setClientManager(nil)
+       assert.Nil(t, ti.getClientManager())
+}
+
+func TestTripleInvoker_IsAvailable(t *testing.T) {
+       tests := []struct {
+               desc          string
+               clientManager *clientManager
+               expect        bool
+       }{
+               {
+                       desc:          "clientManager is nil",
+                       clientManager: nil,
+                       expect:        false,
+               },
+               {
+                       desc: "clientManager is not nil",
+                       clientManager: &clientManager{
+                               isIDL:      true,
+                               triClients: make(map[string]*tri.Client),
+                       },
+                       expect: true,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       url := common.NewURLWithOptions()
+                       ti := newTestTripleInvoker(url, test.clientManager)
+                       assert.Equal(t, test.expect, ti.IsAvailable())
+               })
+       }
+}
+
+func TestTripleInvoker_IsDestroyed(t *testing.T) {
+       tests := []struct {
+               desc          string
+               clientManager *clientManager
+               destroyed     bool
+               expect        bool
+       }{
+               {
+                       desc:          "clientManager is nil",
+                       clientManager: nil,
+                       destroyed:     false,
+                       expect:        false,
+               },
+               {
+                       desc: "clientManager is not nil and not destroyed",
+                       clientManager: &clientManager{
+                               isIDL:      true,
+                               triClients: make(map[string]*tri.Client),
+                       },
+                       destroyed: false,
+                       expect:    false,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       url := common.NewURLWithOptions()
+                       ti := newTestTripleInvoker(url, test.clientManager)
+                       assert.Equal(t, test.expect, ti.IsDestroyed())
+               })
+       }
+}
+
+func TestTripleInvoker_Destroy(t *testing.T) {
+       t.Run("destroy with clientManager", func(t *testing.T) {
+               url := common.NewURLWithOptions()
+               cm := &clientManager{
+                       isIDL:      true,
+                       triClients: make(map[string]*tri.Client),
+               }
+               ti := newTestTripleInvoker(url, cm)
+
+               assert.True(t, ti.IsAvailable())
+               assert.NotNil(t, ti.getClientManager())
+
+               ti.Destroy()
+
+               assert.False(t, ti.IsAvailable())
+               assert.Nil(t, ti.getClientManager())
+       })
+
+       t.Run("destroy without clientManager", func(t *testing.T) {
+               url := common.NewURLWithOptions()
+               ti := newTestTripleInvoker(url, nil)
+
+               ti.Destroy()
+
+               assert.False(t, ti.IsAvailable())
+               assert.Nil(t, ti.getClientManager())
+       })
+
+       t.Run("destroy called multiple times", func(t *testing.T) {
+               url := common.NewURLWithOptions()
+               cm := &clientManager{
+                       isIDL:      true,
+                       triClients: make(map[string]*tri.Client),
+               }
+               ti := newTestTripleInvoker(url, cm)
+
+               // first destroy
+               ti.Destroy()
+               assert.Nil(t, ti.getClientManager())
+
+               // second destroy should not panic
+               ti.Destroy()
+               assert.Nil(t, ti.getClientManager())
+       })
+}
+
+func TestTripleInvoker_Invoke(t *testing.T) {
+       tests := []struct {
+               desc         string
+               setup        func() (*TripleInvoker, base.Invocation)
+               expectErr    error
+               expectErrMsg string
+       }{
+               {
+                       desc: "invoker is destroyed",
+                       setup: func() (*TripleInvoker, base.Invocation) {
+                               url := common.NewURLWithOptions()
+                               ti := newTestTripleInvoker(url, &clientManager{
+                                       isIDL:      true,
+                                       triClients: 
make(map[string]*tri.Client),
+                               })
+                               ti.Destroy()
+                               inv := invocation.NewRPCInvocationWithOptions()
+                               return ti, inv
+                       },
+                       expectErr: base.ErrDestroyedInvoker,
+               },
+               {
+                       desc: "clientManager is nil",
+                       setup: func() (*TripleInvoker, base.Invocation) {
+                               url := common.NewURLWithOptions()
+                               ti := newTestTripleInvoker(url, nil)
+                               inv := invocation.NewRPCInvocationWithOptions()
+                               return ti, inv
+                       },
+                       expectErr: base.ErrClientClosed,
+               },
+               {
+                       desc: "parseInvocation error - miss callType",
+                       setup: func() (*TripleInvoker, base.Invocation) {
+                               url := common.NewURLWithOptions()
+                               ti := newTestTripleInvoker(url, &clientManager{
+                                       isIDL:      true,
+                                       triClients: 
make(map[string]*tri.Client),
+                               })
+                               inv := invocation.NewRPCInvocationWithOptions()
+                               return ti, inv
+                       },
+                       expectErrMsg: "miss CallType",
+               },
+               {
+                       desc: "mergeAttachmentToOutgoing error - invalid 
attachment",
+                       setup: func() (*TripleInvoker, base.Invocation) {
+                               url := common.NewURLWithOptions()
+                               ti := newTestTripleInvoker(url, &clientManager{
+                                       isIDL:      true,
+                                       triClients: 
make(map[string]*tri.Client),
+                               })
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       invocation.WithMethodName("TestMethod"),
+                                       
invocation.WithAttachment("invalid_key", 123), // invalid attachment type
+                               )
+                               inv.SetAttribute(constant.CallTypeKey, 
constant.CallUnary)
+                               return ti, inv
+                       },
+                       expectErrMsg: "invalid",
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       ti, inv := test.setup()
+                       result := ti.Invoke(context.Background(), inv)
+                       if test.expectErr != nil {
+                               assert.Equal(t, test.expectErr, result.Error())
+                       }
+                       if test.expectErrMsg != "" {
+                               assert.NotNil(t, result.Error())
+                               assert.Contains(t, result.Error().Error(), 
test.expectErrMsg)
+                       }
+               })
+       }
+}
+
+func TestTripleInvoker_Invoke_Concurrent(t *testing.T) {
+       url := common.NewURLWithOptions()
+       ti := newTestTripleInvoker(url, &clientManager{
+               isIDL:      true,
+               triClients: make(map[string]*tri.Client),
+       })
+
+       var wg sync.WaitGroup
+       concurrency := 10
+
+       for i := 0; i < concurrency; i++ {
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       inv := invocation.NewRPCInvocationWithOptions()
+                       _ = ti.Invoke(context.Background(), inv)
+               }()
+       }
+
+       wg.Wait()
+}
+
+func Test_mergeAttachmentToOutgoing(t *testing.T) {
+       tests := []struct {
+               desc   string
+               ctx    context.Context
+               invo   func() base.Invocation
+               expect func(t *testing.T, ctx context.Context, err error)
+       }{
+               {
+                       desc: "with timeout attachment",
+                       ctx:  context.Background(),
+                       invo: func() base.Invocation {
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithAttachment(constant.TimeoutKey, "5000"),
+                               )
+                               return inv
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.Nil(t, err)
+                               timeout := ctx.Value(tri.TimeoutKey{})
+                               assert.Equal(t, "5000", timeout)
+                       },
+               },
+               {
+                       desc: "with string attachment",
+                       ctx:  context.Background(),
+                       invo: func() base.Invocation {
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       invocation.WithAttachment("custom-key", 
"custom-value"),
+                               )
+                               return inv
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.Nil(t, err)
+                               header := 
http.Header(tri.ExtractFromOutgoingContext(ctx))
+                               assert.Equal(t, "custom-value", 
header.Get("custom-key"))
+                       },
+               },
+               {
+                       desc: "with string slice attachment",
+                       ctx:  context.Background(),
+                       invo: func() base.Invocation {
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       invocation.WithAttachment("multi-key", 
[]string{"val1", "val2"}),
+                               )
+                               return inv
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.Nil(t, err)
+                               header := 
http.Header(tri.ExtractFromOutgoingContext(ctx))
+                               assert.Equal(t, []string{"val1", "val2"}, 
header.Values("multi-key"))
+                       },
+               },
+               {
+                       desc: "with invalid attachment type",
+                       ctx:  context.Background(),
+                       invo: func() base.Invocation {
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithAttachment("invalid-key", 12345),
+                               )
+                               return inv
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.NotNil(t, err)
+                               assert.Contains(t, err.Error(), "invalid")
+                       },
+               },
+               {
+                       desc: "with empty attachments",
+                       ctx:  context.Background(),
+                       invo: func() base.Invocation {
+                               return invocation.NewRPCInvocationWithOptions()
+                       },
+                       expect: func(t *testing.T, ctx context.Context, err 
error) {
+                               assert.Nil(t, err)
+                       },
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.desc, func(t *testing.T) {
+                       inv := test.invo()
+                       ctx, err := mergeAttachmentToOutgoing(test.ctx, inv)
+                       test.expect(t, ctx, err)
+               })
+       }
+}
diff --git a/protocol/triple/triple_test.go b/protocol/triple/triple_test.go
new file mode 100644
index 000000000..80c73aa21
--- /dev/null
+++ b/protocol/triple/triple_test.go
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package triple
+
+import (
+       "testing"
+)
+
+import (
+       "github.com/stretchr/testify/assert"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3/common/extension"
+)
+
+func TestNewTripleProtocol(t *testing.T) {
+       tp := NewTripleProtocol()
+
+       assert.NotNil(t, tp)
+       assert.NotNil(t, tp.serverMap)
+       assert.Empty(t, tp.serverMap)
+}
+
+func TestGetProtocol(t *testing.T) {
+       // reset singleton for test isolation
+       tripleProtocol = nil
+
+       p1 := GetProtocol()
+       assert.NotNil(t, p1)
+
+       // should return same instance (singleton)
+       p2 := GetProtocol()
+       assert.Same(t, p1, p2)
+}
+
+func TestTripleProtocolRegistration(t *testing.T) {
+       // verify protocol is registered via init()
+       p := extension.GetProtocol(TRIPLE)
+       assert.NotNil(t, p)
+}
+
+func TestTripleConstant(t *testing.T) {
+       assert.Equal(t, "tri", TRIPLE)
+}
+
+func TestTripleProtocol_Destroy_EmptyServerMap(t *testing.T) {
+       tp := NewTripleProtocol()
+
+       // should not panic when serverMap is empty
+       assert.NotPanics(t, func() {
+               tp.Destroy()
+       })
+}


Reply via email to