test(wasm-go/mcp): expand unit test coverage for mcp-server framework (#3871)

Signed-off-by: jingze <daijingze.djz@alibaba-inc.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jingze
2026-06-15 20:29:20 +08:00
committed by GitHub
parent c69526b30e
commit bf0b1e96c5
10 changed files with 3426 additions and 3 deletions

View File

@@ -0,0 +1,323 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed 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 server
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// -----------------------------------------------------------------------------
// NewMcpProtocolHandler
// -----------------------------------------------------------------------------
func TestNewMcpProtocolHandler(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
require.NotNil(t, h)
assert.Equal(t, "http://backend.example/mcp", h.backendURL)
assert.Equal(t, 5000, h.timeout)
assert.Empty(t, h.sessionID, "fresh handler has no session id until Initialize runs")
}
// -----------------------------------------------------------------------------
// parseSSEResponse — fill the remaining branches
// -----------------------------------------------------------------------------
func TestParseSSEResponse_OnlyCommentsAndBlanks(t *testing.T) {
// All non-data lines → must surface "no data field found".
_, err := parseSSEResponse([]byte(": only a comment\n\n: another\n"))
require.Error(t, err)
assert.Contains(t, err.Error(), "no data field")
}
func TestParseSSEResponse_TooLongLine(t *testing.T) {
// Single data line larger than the scanner's 32MB max-token cap.
big := strings.Repeat("x", 33*1024*1024)
_, err := parseSSEResponse([]byte("data: " + big + "\n\n"))
require.Error(t, err)
assert.Contains(t, err.Error(), "32MB", "must surface the max-token overflow as a clear error")
}
func TestParseSSEResponse_MultipleDataLinesReturnsFirst(t *testing.T) {
body := "data: first\n\ndata: second\n\n"
out, err := parseSSEResponse([]byte(body))
require.NoError(t, err)
assert.Equal(t, "first", string(out), "the function returns the first data line and stops")
}
// -----------------------------------------------------------------------------
// createInitializeRequest / createToolsListRequest / createToolsCallRequest
// -----------------------------------------------------------------------------
func TestCreateInitializeRequest_StableShape(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
req := h.createInitializeRequest()
assert.Equal(t, "2.0", req["jsonrpc"])
assert.Equal(t, 1, req["id"])
assert.Equal(t, "initialize", req["method"])
params, ok := req["params"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "2025-03-26", params["protocolVersion"])
clientInfo, ok := params["clientInfo"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "Higress-mcp-proxy", clientInfo["name"])
assert.Equal(t, "1.0.0", clientInfo["version"])
_, hasCaps := params["capabilities"]
assert.True(t, hasCaps)
}
func TestCreateToolsListRequest_NoCursor(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
req := h.createToolsListRequest(nil)
assert.Equal(t, "2.0", req["jsonrpc"])
assert.Equal(t, 2, req["id"])
assert.Equal(t, "tools/list", req["method"])
params, ok := req["params"].(map[string]interface{})
require.True(t, ok)
_, hasCursor := params["cursor"]
assert.False(t, hasCursor, "nil cursor must produce no cursor field")
}
func TestCreateToolsListRequest_EmptyStringCursor(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
empty := ""
req := h.createToolsListRequest(&empty)
params := req["params"].(map[string]interface{})
_, hasCursor := params["cursor"]
assert.False(t, hasCursor, "empty-string cursor is treated as absent")
}
func TestCreateToolsListRequest_WithCursor(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
c := "next-page"
req := h.createToolsListRequest(&c)
params := req["params"].(map[string]interface{})
assert.Equal(t, "next-page", params["cursor"])
}
func TestCreateToolsCallRequest_StableShape(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
args := map[string]interface{}{"q": "hello", "limit": 5}
req := h.createToolsCallRequest("search", args)
assert.Equal(t, "2.0", req["jsonrpc"])
assert.Equal(t, 3, req["id"])
assert.Equal(t, "tools/call", req["method"])
params, ok := req["params"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "search", params["name"])
gotArgs, ok := params["arguments"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "hello", gotArgs["q"])
assert.Equal(t, 5, gotArgs["limit"])
}
func TestCreateToolsCallRequest_NilArguments(t *testing.T) {
h := NewMcpProtocolHandler("http://backend.example/mcp", 5000)
req := h.createToolsCallRequest("noop", nil)
params := req["params"].(map[string]interface{})
assert.Equal(t, "noop", params["name"])
args, ok := params["arguments"]
require.True(t, ok)
assert.Nil(t, args)
}
// -----------------------------------------------------------------------------
// ParseBackendResponse / IsBackendError — extra branches
// -----------------------------------------------------------------------------
func TestParseBackendResponse_StringErrorField(t *testing.T) {
// JSON-RPC error field doesn't have to be an object — anything truthy works.
body := []byte(`{"jsonrpc":"2.0","id":1,"error":"some-text"}`)
resp, isErr, etype := ParseBackendResponse(body)
require.NotNil(t, resp)
assert.True(t, isErr)
assert.Equal(t, "jsonrpc_error", etype)
}
func TestParseBackendResponse_NoResultNoError(t *testing.T) {
// Valid JSON without result/error → not an error, but still parsed.
body := []byte(`{"jsonrpc":"2.0","id":2}`)
resp, isErr, etype := ParseBackendResponse(body)
require.NotNil(t, resp)
assert.False(t, isErr)
assert.Empty(t, etype)
}
func TestParseBackendResponse_ResultIsErrorFalseNotAnError(t *testing.T) {
body := []byte(`{"jsonrpc":"2.0","id":3,"result":{"isError":false}}`)
_, isErr, etype := ParseBackendResponse(body)
assert.False(t, isErr)
assert.Empty(t, etype)
}
func TestParseBackendResponse_ResultNotAnObject(t *testing.T) {
// result is a scalar — the isError-extraction branch is skipped.
body := []byte(`{"jsonrpc":"2.0","id":3,"result":"ok"}`)
_, isErr, etype := ParseBackendResponse(body)
assert.False(t, isErr)
assert.Empty(t, etype)
}
func TestIsBackendError_DelegatesToParse(t *testing.T) {
cases := []struct {
body string
isError bool
etype string
}{
{`{"error":{"code":-1}}`, true, "jsonrpc_error"},
{`{"result":{"isError":true}}`, true, "result_isError"},
{`{"result":{"isError":false}}`, false, ""},
{`not json`, false, ""},
}
for _, c := range cases {
isErr, etype := IsBackendError([]byte(c.body))
assert.Equal(t, c.isError, isErr, "body=%s", c.body)
assert.Equal(t, c.etype, etype, "body=%s", c.body)
}
}
// -----------------------------------------------------------------------------
// McpSessionManagerImpl
// -----------------------------------------------------------------------------
func TestNewMcpSessionManagerImpl(t *testing.T) {
m := NewMcpSessionManagerImpl()
require.NotNil(t, m)
require.NotNil(t, m.sessions)
assert.Empty(t, m.sessions)
}
func TestSessionManager_CreateAndGet(t *testing.T) {
m := NewMcpSessionManagerImpl()
id, err := m.CreateSession("http://backend.example/mcp")
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, "mcp-session-"))
session, ok := m.GetSession(id)
require.True(t, ok)
assert.Equal(t, id, session.ID)
assert.Equal(t, "http://backend.example/mcp", session.BackendURL)
assert.False(t, session.CreatedAt.IsZero())
}
func TestSessionManager_GetSessionUpdatesLastUsed(t *testing.T) {
m := NewMcpSessionManagerImpl()
id, err := m.CreateSession("http://b")
require.NoError(t, err)
// Force a measurable gap so LastUsed changes monotonically.
original := m.sessions[id].LastUsed
time.Sleep(2 * time.Millisecond)
s, ok := m.GetSession(id)
require.True(t, ok)
assert.True(t, s.LastUsed.After(original), "GetSession should refresh LastUsed")
}
func TestSessionManager_GetUnknownSession(t *testing.T) {
m := NewMcpSessionManagerImpl()
s, ok := m.GetSession("missing")
assert.False(t, ok)
assert.Nil(t, s)
}
func TestSessionManager_CleanupSession_Existing(t *testing.T) {
m := NewMcpSessionManagerImpl()
id, _ := m.CreateSession("http://b")
m.CleanupSession(id)
_, ok := m.GetSession(id)
assert.False(t, ok)
}
func TestSessionManager_CleanupSession_NonExistent(t *testing.T) {
m := NewMcpSessionManagerImpl()
// Must not panic on unknown id.
m.CleanupSession("never-existed")
assert.Empty(t, m.sessions)
}
func TestSessionManager_CleanupExpiredSessions(t *testing.T) {
m := NewMcpSessionManagerImpl()
fresh, _ := m.CreateSession("fresh")
stale, _ := m.CreateSession("stale")
// Backdate the stale session.
m.sessions[stale].LastUsed = time.Now().Add(-10 * time.Minute)
m.CleanupExpiredSessions(1 * time.Minute)
_, freshOk := m.sessions[fresh]
_, staleOk := m.sessions[stale]
assert.True(t, freshOk, "fresh session must remain")
assert.False(t, staleOk, "stale session must be removed")
}
func TestSessionManager_CleanupExpiredSessions_EmptyMap(t *testing.T) {
m := NewMcpSessionManagerImpl()
// Must not panic on empty manager.
m.CleanupExpiredSessions(1 * time.Second)
assert.Empty(t, m.sessions)
}
func TestSessionManager_CreateSessionsAreUnique(t *testing.T) {
m := NewMcpSessionManagerImpl()
id1, _ := m.CreateSession("http://b")
// Guarantee a different nanosecond timestamp.
time.Sleep(1 * time.Millisecond)
id2, _ := m.CreateSession("http://b")
assert.NotEqual(t, id1, id2, "session IDs should be unique")
}
// -----------------------------------------------------------------------------
// ensureHeader
// -----------------------------------------------------------------------------
func TestEnsureHeader_AddsWhenMissing(t *testing.T) {
headers := [][2]string{{"X-Other", "v"}}
ensureHeader(&headers, "X-New", "value")
require.Len(t, headers, 2)
assert.Equal(t, [2]string{"X-New", "value"}, headers[1])
}
func TestEnsureHeader_ReplacesCaseInsensitively(t *testing.T) {
headers := [][2]string{{"content-type", "text/plain"}}
ensureHeader(&headers, "Content-Type", "application/json")
require.Len(t, headers, 1)
// Replace path rewrites the original casing too.
assert.Equal(t, "Content-Type", headers[0][0])
assert.Equal(t, "application/json", headers[0][1])
}
func TestEnsureHeader_NoDuplicateOnRepeatedCalls(t *testing.T) {
headers := [][2]string{}
ensureHeader(&headers, "X-K", "1")
ensureHeader(&headers, "X-K", "2")
require.Len(t, headers, 1)
assert.Equal(t, "2", headers[0][1])
}