Simplify variant dispatch and session by replacing per-method boilerplate with generic handlers backed by reflect. ## Motivation and Context The dispatcher and backend session had separate handler functions for every MCP method (ListTools, ListResources, CallTool, Subscribe, etc.), each following an identical pattern: extract params, inject metadata, call inner handler, cast result. This meant ~250 lines of near-identical code across dispatch.go and session.go, and every new MCP method required adding another copy. ## How Has This Been Tested? All existing unit and integration tests pass (go test -v ./...) ## Breaking Changes None. All changes are internal to the variants package. Public API is unchanged. ## Types of changes Refactor (non-breaking change which fixes an issue)
684 lines
19 KiB
Go
684 lines
19 KiB
Go
// Copyright 2025 The MCP Variants Authors. All rights reserved.
|
|
// Use of this source code is governed by a Apache-2.0
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package variants
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// isNilInterface
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIsNilInterface(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
val any
|
|
want bool
|
|
}{
|
|
{"untyped nil", nil, true},
|
|
{"typed nil pointer", (*mcp.ListToolsParams)(nil), true},
|
|
{"non-nil pointer", &mcp.ListToolsParams{}, false},
|
|
{"non-pointer value", 42, false},
|
|
{"string value", "hello", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, isNilInterface(tt.val))
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handleList
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// newTestDispatcher builds a dispatcher with a single variant backed by a
|
|
// custom mcpMethodHandler. The handler receives the request exactly as the
|
|
// dispatcher sends it, so tests can inspect params/session mutations.
|
|
func newTestDispatcher(variantID string, handler mcp.MethodHandler) *dispatcher {
|
|
return &dispatcher{
|
|
server: NewServer(&mcp.Implementation{Name: "test", Version: "v0.0.1"}).
|
|
WithVariant(ServerVariant{ID: variantID, Status: Stable}, mcp.NewServer(&mcp.Implementation{Name: "inner", Version: "v0.0.1"}, nil), 0),
|
|
connections: map[string]*innerConnection{
|
|
variantID: {
|
|
backendSession: &backendSession{
|
|
variantID: variantID,
|
|
mcpMethodHandler: handler,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestHandleList_NilResult(t *testing.T) {
|
|
const variantID = "test-variant"
|
|
|
|
tests := []struct {
|
|
name string
|
|
handler func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error)
|
|
}{
|
|
{
|
|
name: "untyped nil",
|
|
handler: func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
return nil, nil
|
|
},
|
|
},
|
|
{
|
|
name: "typed nil",
|
|
handler: func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
return (*mcp.ListToolsResult)(nil), nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
d := newTestDispatcher(variantID, tt.handler)
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
},
|
|
}
|
|
|
|
result, err := d.handleList(context.Background(), "tools/list", req)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleList_CursorRoundTrip(t *testing.T) {
|
|
const variantID = "v1"
|
|
const innerCursor = "page-2-token"
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
req func(wrappedCursor string) mcp.Request
|
|
result func() mcp.Result // result returned by backend with NextCursor set
|
|
}{
|
|
{
|
|
name: "tools/list",
|
|
method: "tools/list",
|
|
req: func(c string) mcp.Request {
|
|
return &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Cursor: c,
|
|
},
|
|
}
|
|
},
|
|
result: func() mcp.Result {
|
|
return &mcp.ListToolsResult{NextCursor: "next-inner"}
|
|
},
|
|
},
|
|
{
|
|
name: "resources/list",
|
|
method: "resources/list",
|
|
req: func(c string) mcp.Request {
|
|
return &mcp.ListResourcesRequest{
|
|
Params: &mcp.ListResourcesParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Cursor: c,
|
|
},
|
|
}
|
|
},
|
|
result: func() mcp.Result {
|
|
return &mcp.ListResourcesResult{NextCursor: "next-inner"}
|
|
},
|
|
},
|
|
{
|
|
name: "prompts/list",
|
|
method: "prompts/list",
|
|
req: func(c string) mcp.Request {
|
|
return &mcp.ListPromptsRequest{
|
|
Params: &mcp.ListPromptsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Cursor: c,
|
|
},
|
|
}
|
|
},
|
|
result: func() mcp.Result {
|
|
return &mcp.ListPromptsResult{NextCursor: "next-inner"}
|
|
},
|
|
},
|
|
{
|
|
name: "resources/templates/list",
|
|
method: "resources/templates/list",
|
|
req: func(c string) mcp.Request {
|
|
return &mcp.ListResourceTemplatesRequest{
|
|
Params: &mcp.ListResourceTemplatesParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Cursor: c,
|
|
},
|
|
}
|
|
},
|
|
result: func() mcp.Result {
|
|
return &mcp.ListResourceTemplatesResult{NextCursor: "next-inner"}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
wrappedCursor := wrapCursor(innerCursor, variantID)
|
|
var receivedCursor string
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
// Capture what cursor the backend actually receives.
|
|
params := req.GetParams()
|
|
f := reflect.ValueOf(params).Elem().FieldByName("Cursor")
|
|
receivedCursor = f.String()
|
|
return tt.result(), nil
|
|
})
|
|
|
|
result, err := d.handleList(context.Background(), tt.method, tt.req(wrappedCursor))
|
|
require.NoError(t, err)
|
|
|
|
// Backend should have received the inner (unwrapped) cursor.
|
|
assert.Equal(t, innerCursor, receivedCursor, "cursor should be unwrapped before reaching backend")
|
|
|
|
// Result's NextCursor should now be wrapped.
|
|
nextCursor := reflect.ValueOf(result).Elem().FieldByName("NextCursor").String()
|
|
unwrapped, err := unwrapCursor(nextCursor, variantID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "next-inner", unwrapped, "NextCursor should be wrapped with variant ID")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleList_NoCursor(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
return &mcp.ListToolsResult{}, nil
|
|
})
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
},
|
|
}
|
|
|
|
result, err := d.handleList(context.Background(), "tools/list", req)
|
|
require.NoError(t, err)
|
|
|
|
// No NextCursor should be set when backend returns empty cursor.
|
|
r := result.(*mcp.ListToolsResult)
|
|
assert.Empty(t, r.NextCursor)
|
|
}
|
|
|
|
func TestHandleList_InvalidCursor(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
t.Fatal("backend should not be called with invalid cursor")
|
|
return nil, nil
|
|
})
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Cursor: "not-valid-base64!@#$",
|
|
},
|
|
}
|
|
|
|
_, err := d.handleList(context.Background(), "tools/list", req)
|
|
require.Error(t, err)
|
|
|
|
var jErr *jsonrpc.Error
|
|
require.True(t, errors.As(err, &jErr))
|
|
assert.EqualValues(t, jsonrpc.CodeInvalidParams, jErr.Code)
|
|
}
|
|
|
|
func TestHandleList_CrossVariantCursor(t *testing.T) {
|
|
const variantID = "v1"
|
|
otherVariantCursor := wrapCursor("page2", "other-variant")
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
t.Fatal("backend should not be called with cross-variant cursor")
|
|
return nil, nil
|
|
})
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Cursor: otherVariantCursor,
|
|
},
|
|
}
|
|
|
|
_, err := d.handleList(context.Background(), "tools/list", req)
|
|
require.Error(t, err)
|
|
|
|
var jErr *jsonrpc.Error
|
|
require.True(t, errors.As(err, &jErr))
|
|
assert.EqualValues(t, jsonrpc.CodeInvalidParams, jErr.Code)
|
|
assert.Contains(t, jErr.Message, "Cursor invalid for requested variant")
|
|
}
|
|
|
|
func TestHandleList_ErrorEnrichment(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
backendErr := &jsonrpc.Error{
|
|
Code: jsonrpc.CodeInvalidParams,
|
|
Message: "unknown tool",
|
|
}
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
return nil, backendErr
|
|
})
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
},
|
|
}
|
|
|
|
_, err := d.handleList(context.Background(), "tools/list", req)
|
|
require.Error(t, err)
|
|
|
|
var jErr *jsonrpc.Error
|
|
require.True(t, errors.As(err, &jErr))
|
|
assert.Contains(t, string(jErr.Data), `"activeVariant"`)
|
|
assert.Contains(t, string(jErr.Data), variantID)
|
|
}
|
|
|
|
func TestHandleList_NilParams(t *testing.T) {
|
|
const variantID = "v1"
|
|
called := false
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
called = true
|
|
return &mcp.ListToolsResult{}, nil
|
|
})
|
|
|
|
// Typed-nil params — the SDK produces these for parameterless requests.
|
|
req := &mcp.ListToolsRequest{
|
|
Params: (*mcp.ListToolsParams)(nil),
|
|
}
|
|
|
|
_, err := d.handleList(context.Background(), "tools/list", req)
|
|
require.NoError(t, err)
|
|
assert.True(t, called, "backend should still be called with nil params")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handleDirect
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandleDirect_MethodRouting(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
req mcp.Request
|
|
}{
|
|
{
|
|
name: "tools/call",
|
|
method: "tools/call",
|
|
req: &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Name: "my-tool",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "resources/read",
|
|
method: "resources/read",
|
|
req: &mcp.ReadResourceRequest{
|
|
Params: &mcp.ReadResourceParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
URI: "file:///test",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "prompts/get",
|
|
method: "prompts/get",
|
|
req: &mcp.GetPromptRequest{
|
|
Params: &mcp.GetPromptParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Name: "my-prompt",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "resources/subscribe",
|
|
method: "resources/subscribe",
|
|
req: &mcp.SubscribeRequest{
|
|
Params: &mcp.SubscribeParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
URI: "file:///watch",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "resources/unsubscribe",
|
|
method: "resources/unsubscribe",
|
|
req: &mcp.UnsubscribeRequest{
|
|
Params: &mcp.UnsubscribeParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
URI: "file:///watch",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "completion/complete",
|
|
method: "completion/complete",
|
|
req: &mcp.CompleteRequest{
|
|
Params: &mcp.CompleteParams{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var receivedMethod string
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
receivedMethod = method
|
|
return nil, nil
|
|
})
|
|
|
|
_, err := d.handleDirect(context.Background(), tt.method, tt.req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.method, receivedMethod, "backend should receive the correct method")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleDirect_MetaInjection(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
var receivedMeta mcp.Meta
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
receivedMeta = req.GetParams().GetMeta()
|
|
return &mcp.CallToolResult{}, nil
|
|
})
|
|
|
|
// Start without variant meta — handleDirect should inject it.
|
|
req := &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{
|
|
Name: "my-tool",
|
|
},
|
|
}
|
|
|
|
_, err := d.handleDirect(context.Background(), "tools/call", req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, variantID, receivedMeta[metaKeyVariant], "variant meta should be injected into params")
|
|
}
|
|
|
|
func TestHandleDirect_ErrorEnrichment(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
return nil, &jsonrpc.Error{
|
|
Code: jsonrpc.CodeInvalidParams,
|
|
Message: "tool not found",
|
|
}
|
|
})
|
|
|
|
req := &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Name: "nonexistent",
|
|
},
|
|
}
|
|
|
|
_, err := d.handleDirect(context.Background(), "tools/call", req)
|
|
require.Error(t, err)
|
|
|
|
var jErr *jsonrpc.Error
|
|
require.True(t, errors.As(err, &jErr))
|
|
assert.Contains(t, string(jErr.Data), `"activeVariant"`)
|
|
assert.Contains(t, string(jErr.Data), variantID)
|
|
}
|
|
|
|
func TestHandleDirect_NonEnrichableError(t *testing.T) {
|
|
const variantID = "v1"
|
|
plainErr := errors.New("internal error")
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
return nil, plainErr
|
|
})
|
|
|
|
req := &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{
|
|
Meta: mcp.Meta{metaKeyVariant: variantID},
|
|
Name: "my-tool",
|
|
},
|
|
}
|
|
|
|
_, err := d.handleDirect(context.Background(), "tools/call", req)
|
|
assert.Equal(t, plainErr, err, "non-jsonrpc errors should pass through unmodified")
|
|
}
|
|
|
|
func TestHandleDirect_NilParams(t *testing.T) {
|
|
const variantID = "v1"
|
|
called := false
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
called = true
|
|
return nil, nil
|
|
})
|
|
|
|
req := &mcp.SubscribeRequest{
|
|
Params: (*mcp.SubscribeParams)(nil),
|
|
}
|
|
|
|
_, err := d.handleDirect(context.Background(), "resources/subscribe", req)
|
|
require.NoError(t, err)
|
|
assert.True(t, called, "backend should still be called when params are typed-nil")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handle (top-level router)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandle_UnknownMethodPassthrough(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
t.Fatal("dispatcher handler should not be called for unknown methods")
|
|
return nil, nil
|
|
})
|
|
|
|
nextCalled := false
|
|
next := func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
nextCalled = true
|
|
return nil, nil
|
|
}
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{},
|
|
}
|
|
|
|
_, err := d.handle(context.Background(), "custom/method", req, next)
|
|
require.NoError(t, err)
|
|
assert.True(t, nextCalled, "unknown methods should fall through to next")
|
|
}
|
|
|
|
func TestHandle_RoutesToCorrectHandler(t *testing.T) {
|
|
const variantID = "v1"
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
req mcp.Request
|
|
}{
|
|
{"list routes to handleList", "tools/list", &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{Meta: mcp.Meta{metaKeyVariant: variantID}},
|
|
}},
|
|
{"call routes to handleDirect", "tools/call", &mcp.CallToolRequest{
|
|
Params: &mcp.CallToolParamsRaw{Meta: mcp.Meta{metaKeyVariant: variantID}, Name: "t"},
|
|
}},
|
|
{"subscribe routes to handleDirect", "resources/subscribe", &mcp.SubscribeRequest{
|
|
Params: &mcp.SubscribeParams{Meta: mcp.Meta{metaKeyVariant: variantID}, URI: "u"},
|
|
}},
|
|
{"unsubscribe routes to handleDirect", "resources/unsubscribe", &mcp.UnsubscribeRequest{
|
|
Params: &mcp.UnsubscribeParams{Meta: mcp.Meta{metaKeyVariant: variantID}, URI: "u"},
|
|
}},
|
|
{"complete routes to handleDirect", "completion/complete", &mcp.CompleteRequest{
|
|
Params: &mcp.CompleteParams{Meta: mcp.Meta{metaKeyVariant: variantID}},
|
|
}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var receivedMethod string
|
|
d := newTestDispatcher(variantID, func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
receivedMethod = method
|
|
if method == "tools/list" {
|
|
return &mcp.ListToolsResult{}, nil
|
|
}
|
|
return nil, nil
|
|
})
|
|
|
|
next := func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
t.Fatalf("next should not be called for method %s", method)
|
|
return nil, nil
|
|
}
|
|
|
|
_, err := d.handle(context.Background(), tt.method, tt.req, next)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.method, receivedMethod)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// handleReceive (session.go)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestHandleReceive_SessionInjection(t *testing.T) {
|
|
originalSession := &mcp.ServerSession{}
|
|
targetSession := &mcp.ServerSession{}
|
|
|
|
var receivedSession *mcp.ServerSession
|
|
bs := &backendSession{
|
|
variantID: "v1",
|
|
serverSession: targetSession,
|
|
mcpMethodHandler: func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
|
|
receivedSession = req.GetSession().(*mcp.ServerSession)
|
|
return &mcp.ListToolsResult{}, nil
|
|
},
|
|
}
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Session: originalSession,
|
|
Params: &mcp.ListToolsParams{},
|
|
}
|
|
|
|
_, err := bs.handleReceive(context.Background(), "tools/list", req)
|
|
require.NoError(t, err)
|
|
assert.Same(t, targetSession, receivedSession, "handleReceive should replace Session with inner server session")
|
|
assert.Same(t, originalSession, req.Session, "handleReceive should not mutate the original request")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getConnection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestGetConnection_InvalidVariant(t *testing.T) {
|
|
d := newTestDispatcher("v1", nil)
|
|
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{
|
|
Meta: mcp.Meta{metaKeyVariant: "nonexistent"},
|
|
},
|
|
}
|
|
|
|
_, err := d.getConnection(context.Background(), req)
|
|
require.Error(t, err)
|
|
|
|
var jErr *jsonrpc.Error
|
|
require.True(t, errors.As(err, &jErr))
|
|
assert.EqualValues(t, jsonrpc.CodeInvalidParams, jErr.Code)
|
|
assert.Contains(t, jErr.Message, "Invalid server variant")
|
|
assert.Contains(t, string(jErr.Data), "nonexistent")
|
|
}
|
|
|
|
func TestGetConnection_DefaultVariant(t *testing.T) {
|
|
d := newTestDispatcher("v1", nil)
|
|
|
|
// No variant in meta — should fall back to default (first-ranked).
|
|
req := &mcp.ListToolsRequest{
|
|
Params: &mcp.ListToolsParams{},
|
|
}
|
|
|
|
conn, err := d.getConnection(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "v1", conn.backendSession.variantID)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// enrichError
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestEnrichError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
wantEnrich bool
|
|
}{
|
|
{
|
|
name: "InvalidParams enriched",
|
|
err: &jsonrpc.Error{
|
|
Code: jsonrpc.CodeInvalidParams,
|
|
Message: "bad param",
|
|
},
|
|
wantEnrich: true,
|
|
},
|
|
{
|
|
name: "MethodNotFound enriched",
|
|
err: &jsonrpc.Error{
|
|
Code: jsonrpc.CodeMethodNotFound,
|
|
Message: "no method",
|
|
},
|
|
wantEnrich: true,
|
|
},
|
|
{
|
|
name: "InternalError not enriched",
|
|
err: &jsonrpc.Error{
|
|
Code: jsonrpc.CodeInternalError,
|
|
Message: "server error",
|
|
},
|
|
wantEnrich: false,
|
|
},
|
|
{
|
|
name: "plain error not enriched",
|
|
err: errors.New("some error"),
|
|
wantEnrich: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := enrichError(tt.err, "v1")
|
|
|
|
var jErr *jsonrpc.Error
|
|
if errors.As(result, &jErr) && tt.wantEnrich {
|
|
assert.Contains(t, string(jErr.Data), `"activeVariant"`)
|
|
assert.Contains(t, string(jErr.Data), `"v1"`)
|
|
} else if !tt.wantEnrich {
|
|
if errors.As(result, &jErr) {
|
|
// Should NOT have activeVariant
|
|
assert.NotContains(t, string(jErr.Data), `"activeVariant"`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|