Files
Cyrene_v2/internal/capability/mcp/client_test.go
T

248 lines
7.1 KiB
Go

package mcp
import (
"context"
"errors"
"testing"
)
func TestClient_New(t *testing.T) {
t.Parallel()
c := New(MCPConfig{Name: "calc", Command: "mcp-calc"})
if c.cfg.Name != "calc" {
t.Fatalf("cfg.Name = %q, want calc", c.cfg.Name)
}
if c.state != stateNew {
t.Fatalf("state = %v, want stateNew", c.state)
}
}
func TestClient_Connect_FakeTransport(t *testing.T) {
t.Parallel()
ft := &fakeTransport{}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
if ft.starts != 1 {
t.Fatalf("starts = %d, want 1", ft.starts)
}
if !ft.hasMethod("initialize") {
t.Fatalf("initialize was not called; methods = %v", ft.calledMethods())
}
if c.state != stateConnected {
t.Fatalf("state = %v, want stateConnected", c.state)
}
if c.info.Name != "fake-server" {
t.Fatalf("info.Name = %q, want fake-server", c.info.Name)
}
}
func TestClient_Connect_Idempotent(t *testing.T) {
t.Parallel()
ft := &fakeTransport{}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("first Connect: %v", err)
}
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("second Connect: %v", err)
}
if ft.starts != 1 {
t.Fatalf("starts = %d, want 1 (second connect must not restart)", ft.starts)
}
}
func TestClient_Connect_NoTransport(t *testing.T) {
t.Parallel()
c := New(MCPConfig{Name: "empty"})
err := c.Connect(context.Background())
if !errors.Is(err, ErrNoTransport) {
t.Fatalf("Connect error = %v, want ErrNoTransport", err)
}
}
func TestClient_Connect_Closed(t *testing.T) {
t.Parallel()
ft := &fakeTransport{}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
err := c.Connect(context.Background())
if !errors.Is(err, ErrClosed) {
t.Fatalf("Connect error = %v, want ErrClosed", err)
}
}
func TestClient_Connect_StartError(t *testing.T) {
t.Parallel()
ft := &fakeTransport{startErr: errors.New("boom")}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err == nil {
t.Fatal("Connect succeeded, want error")
}
if ft.closes != 1 {
t.Fatalf("closes = %d, want 1 (failed Start must Close the transport)", ft.closes)
}
}
func TestClient_Connect_InitError(t *testing.T) {
t.Parallel()
ft := &fakeTransport{initErr: errors.New("init boom")}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err == nil {
t.Fatal("Connect succeeded, want error")
}
if ft.closes != 1 {
t.Fatalf("closes = %d, want 1 (failed initialize must Close the transport)", ft.closes)
}
}
func TestClient_ListTools(t *testing.T) {
t.Parallel()
ft := &fakeTransport{
tools: []ToolInfo{{Name: "add", Description: "Adds", InputSchema: map[string]any{"type": "object"}}},
}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
tools, err := c.ListTools(context.Background())
if err != nil {
t.Fatalf("ListTools: %v", err)
}
if len(tools) != 1 || tools[0].Name != "add" || tools[0].Description != "Adds" {
t.Fatalf("tools = %+v, want [add/Adds]", tools)
}
}
func TestClient_ListTools_NotConnected(t *testing.T) {
t.Parallel()
c := New(MCPConfig{Name: "s", Command: "x"})
_, err := c.ListTools(context.Background())
if !errors.Is(err, ErrNotConnected) {
t.Fatalf("ListTools error = %v, want ErrNotConnected", err)
}
}
func TestClient_CallTool(t *testing.T) {
t.Parallel()
ft := &fakeTransport{}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
res, err := c.CallTool(context.Background(), "add", map[string]any{"x": 1})
if err != nil {
t.Fatalf("CallTool: %v", err)
}
if res.IsError {
t.Fatal("IsError = true, want false")
}
if len(res.Content) != 1 || res.Content[0].Text != "hello" {
t.Fatalf("Content = %+v, want [hello]", res.Content)
}
}
func TestClient_CallTool_BadArgs(t *testing.T) {
t.Parallel()
ft := &fakeTransport{}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
_, err := c.CallTool(context.Background(), "add", "not-a-map")
if err == nil {
t.Fatal("CallTool with non-object args succeeded, want error")
}
}
func TestClient_CallTool_NotConnected(t *testing.T) {
t.Parallel()
c := New(MCPConfig{Name: "s", Command: "x"})
_, err := c.CallTool(context.Background(), "add", map[string]any{})
if !errors.Is(err, ErrNotConnected) {
t.Fatalf("CallTool error = %v, want ErrNotConnected", err)
}
}
func TestClient_DiscoverTools_MapsToSDK(t *testing.T) {
t.Parallel()
ft := &fakeTransport{
tools: []ToolInfo{{Name: "add", Description: "Adds", InputSchema: map[string]any{"type": "object"}}},
}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
tools, err := c.DiscoverTools(context.Background())
if err != nil {
t.Fatalf("DiscoverTools: %v", err)
}
if len(tools) != 1 {
t.Fatalf("len = %d, want 1", len(tools))
}
if tools[0].Name() != "add" || tools[0].Description() != "Adds" {
t.Fatalf("tool = %q/%q, want add/Adds", tools[0].Name(), tools[0].Description())
}
params, err := tools[0].Parameters()
if err != nil {
t.Fatalf("Parameters: %v", err)
}
if params["type"] != "object" {
t.Fatalf("params[type] = %v, want object", params["type"])
}
out, err := tools[0].Execute(context.Background(), map[string]any{"x": 1})
if err != nil {
t.Fatalf("Execute: %v", err)
}
if out != "hello" {
t.Fatalf("Execute result = %v, want hello", out)
}
}
func TestClient_DiscoverTools_NilSchema(t *testing.T) {
t.Parallel()
ft := &fakeTransport{tools: []ToolInfo{{Name: "nop", InputSchema: nil}}}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
tools, err := c.DiscoverTools(context.Background())
if err != nil {
t.Fatalf("DiscoverTools: %v", err)
}
params, err := tools[0].Parameters()
if err != nil {
t.Fatalf("Parameters: %v", err)
}
if params["type"] != "object" {
t.Fatalf("params[type] = %v, want object", params["type"])
}
}
func TestClient_Close_ClosesTransportAndForbidsUse(t *testing.T) {
t.Parallel()
ft := &fakeTransport{}
c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft))
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
if err := c.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if ft.closes != 1 {
t.Fatalf("closes = %d, want 1", ft.closes)
}
if err := c.Close(); err != nil {
t.Fatalf("second Close: %v", err)
}
if ft.closes != 1 {
t.Fatalf("closes = %d, want 1 after idempotent Close", ft.closes)
}
if _, err := c.ListTools(context.Background()); !errors.Is(err, ErrNotConnected) {
t.Fatalf("ListTools after Close = %v, want ErrNotConnected", err)
}
}