Files
gnoma/internal/mcp/manager_test.go
vikingowl d7b524664d fix(m8): replace_default map, error UX, benchmarks, and launch prep
- Fix replace_default positional bug: []string → map[string]string for
  explicit MCP tool → built-in name mapping
- Improve error messages for missing API keys (3 actionable options) and
  unknown providers (early validation with available list)
- Remove python3 dependency from MCP tests (pure bash grep/sed parsing)
- Add router benchmark scaffold (6 benchmarks in bench_test.go + docs)
- Add .goreleaser.yml for cross-platform binary releases with ldflags
- Add launch-ready README with quickstart, extensibility docs, GIF placeholder
- Add CONTRIBUTING.md and Gitea issue templates (bug report, feature request)
2026-04-12 03:34:58 +02:00

210 lines
6.3 KiB
Go

package mcp
import (
"context"
"encoding/json"
"log/slog"
"os"
"testing"
"time"
"somegit.dev/Owlibou/gnoma/internal/tool"
)
func TestManager_StartAll_RegistersTools(t *testing.T) {
tools := []MCPTool{
{Name: "status", Description: "Get status", InputSchema: json.RawMessage(`{"type":"object"}`)},
{Name: "commit", Description: "Create commit", InputSchema: json.RawMessage(`{"type":"object"}`)},
}
callResult := `{"content":[{"type":"text","text":"ok"}]}`
script := writeMCPServer(t, tools, callResult)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
reg := tool.NewRegistry()
mgr := NewManager(logger)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := mgr.StartAll(ctx, []ServerConfig{
{
Name: "git",
Command: "bash",
Args: []string{script},
Timeout: 5 * time.Second,
},
}, reg)
if err != nil {
t.Fatalf("StartAll: %v", err)
}
defer mgr.Shutdown()
// Tools should be registered with mcp__ prefix.
if _, ok := reg.Get("mcp__git__status"); !ok {
t.Error("mcp__git__status not found in registry")
}
if _, ok := reg.Get("mcp__git__commit"); !ok {
t.Error("mcp__git__commit not found in registry")
}
}
func TestManager_StartAll_ReplaceDefault(t *testing.T) {
tools := []MCPTool{
{Name: "exec", Description: "Custom bash", InputSchema: json.RawMessage(`{"type":"object"}`)},
}
callResult := `{"content":[{"type":"text","text":"replaced"}]}`
script := writeMCPServer(t, tools, callResult)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
reg := tool.NewRegistry()
// Register a mock built-in "bash" tool first.
reg.Register(&mockTool{name: "bash"})
mgr := NewManager(logger)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := mgr.StartAll(ctx, []ServerConfig{
{
Name: "custom",
Command: "bash",
Args: []string{script},
Timeout: 5 * time.Second,
ReplaceDefault: map[string]string{"exec": "bash"},
},
}, reg)
if err != nil {
t.Fatalf("StartAll: %v", err)
}
defer mgr.Shutdown()
// The "bash" tool should now be the MCP adapter, not the mock.
bashTool, ok := reg.Get("bash")
if !ok {
t.Fatal("bash tool not found after replace")
}
adapter, ok := bashTool.(*Adapter)
if !ok {
t.Fatalf("bash tool is %T, want *Adapter", bashTool)
}
if adapter.mcpTool.Name != "exec" {
t.Errorf("replaced tool's MCP name = %q, want %q", adapter.mcpTool.Name, "exec")
}
}
func TestManager_StartAll_BadCommand(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
reg := tool.NewRegistry()
mgr := NewManager(logger)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := mgr.StartAll(ctx, []ServerConfig{
{
Name: "bad",
Command: "/nonexistent/binary/that/does/not/exist",
Timeout: 2 * time.Second,
},
}, reg)
if err == nil {
t.Error("expected error for bad command")
mgr.Shutdown()
}
}
func TestManager_Shutdown(t *testing.T) {
tools := []MCPTool{
{Name: "ping", Description: "Ping", InputSchema: json.RawMessage(`{"type":"object"}`)},
}
script := writeMCPServer(t, tools, `{"content":[]}`)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
reg := tool.NewRegistry()
mgr := NewManager(logger)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := mgr.StartAll(ctx, []ServerConfig{
{
Name: "test",
Command: "bash",
Args: []string{script},
Timeout: 5 * time.Second,
},
}, reg)
if err != nil {
t.Fatalf("StartAll: %v", err)
}
// Shutdown should not error.
if err := mgr.Shutdown(); err != nil {
t.Errorf("Shutdown: %v", err)
}
}
func TestManager_StartAll_ReplaceDefault_PicksMatchingTool(t *testing.T) {
// Server has multiple tools, only one replaces a built-in.
tools := []MCPTool{
{Name: "read", Description: "Read file", InputSchema: json.RawMessage(`{"type":"object"}`)},
{Name: "write", Description: "Write file", InputSchema: json.RawMessage(`{"type":"object"}`)},
{Name: "extra", Description: "Extra tool", InputSchema: json.RawMessage(`{"type":"object"}`)},
}
script := writeMCPServer(t, tools, `{"content":[]}`)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
reg := tool.NewRegistry()
reg.Register(&mockTool{name: "fs.read"})
reg.Register(&mockTool{name: "fs.write"})
mgr := NewManager(logger)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := mgr.StartAll(ctx, []ServerConfig{
{
Name: "custom-fs",
Command: "bash",
Args: []string{script},
Timeout: 5 * time.Second,
ReplaceDefault: map[string]string{"read": "fs.read", "write": "fs.write"},
},
}, reg)
if err != nil {
t.Fatalf("StartAll: %v", err)
}
defer mgr.Shutdown()
// fs.read and fs.write should be replaced.
if fsRead, ok := reg.Get("fs.read"); !ok {
t.Error("fs.read not found")
} else if _, ok := fsRead.(*Adapter); !ok {
t.Error("fs.read should be replaced by MCP adapter")
}
if fsWrite, ok := reg.Get("fs.write"); !ok {
t.Error("fs.write not found")
} else if _, ok := fsWrite.(*Adapter); !ok {
t.Error("fs.write should be replaced by MCP adapter")
}
// "extra" should be registered with mcp__ prefix.
if _, ok := reg.Get("mcp__custom-fs__extra"); !ok {
t.Error("mcp__custom-fs__extra not found in registry")
}
}
// mockTool is a minimal tool.Tool for testing registry replacement.
type mockTool struct {
name string
}
func (m *mockTool) Name() string { return m.name }
func (m *mockTool) Description() string { return "mock" }
func (m *mockTool) Parameters() json.RawMessage { return json.RawMessage(`{}`) }
func (m *mockTool) Execute(_ context.Context, _ json.RawMessage) (tool.Result, error) { return tool.Result{}, nil }
func (m *mockTool) IsReadOnly() bool { return false }
func (m *mockTool) IsDestructive() bool { return false }