mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-07 22:06:28 +00:00
Tests / Integration Tests (push) Skipped
Build , Vet Test, and Lint / Build (push) Successful in 1m33s
Tests / Unit Tests (push) Successful in 2m1s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m36s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m38s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m49s
Tests / Race Detector (push) Successful in 4m31s
BREAKING CHANGE: Config.ReadOnly is now a *bool and unset means read-only. Use ReadOnly: resolvemcp.Bool(false) to enable insert/update/delete, annotations and function calls. Adds the Bool helper and updates docs.
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
package resolvemcp
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
|
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
|
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
|
)
|
|
|
|
func newReadOnlyHandler(t *testing.T) *Handler {
|
|
t.Helper()
|
|
h := NewHandler(database.NewPgSQLAdapter(nil), modelregistry.NewModelRegistry(),
|
|
Config{EnableAnnotations: true})
|
|
if err := h.RegisterModel("public", "items", &docItem{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func TestReadOnlyToolSet(t *testing.T) {
|
|
h := newReadOnlyHandler(t)
|
|
tools := h.mcpServer.ListTools()
|
|
for _, name := range []string{"list_tables", "describe_table", "select_table"} {
|
|
if tools[name] == nil {
|
|
t.Errorf("read tool %s missing", name)
|
|
}
|
|
}
|
|
for _, name := range []string{"insert_into_table", "update_table", "delete_from_table", "call_function", "list_functions", annotationToolName} {
|
|
if tools[name] != nil {
|
|
t.Errorf("tool %s must not be registered on a read-only server", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadOnlyRefusesWritesAndReportsIt(t *testing.T) {
|
|
h := newReadOnlyHandler(t)
|
|
ctx := context.Background()
|
|
args := map[string]any{"table": "public.items", "data": map[string]any{"name": "x"}, "id": 1}
|
|
|
|
for name, fn := range map[string]func() map[string]any{
|
|
"insert": func() map[string]any { r, _ := h.handleInsert(ctx, callReq(args)); return payload(t, r) },
|
|
"update": func() map[string]any { r, _ := h.handleUpdate(ctx, callReq(args)); return payload(t, r) },
|
|
"delete": func() map[string]any { r, _ := h.handleDelete(ctx, callReq(args)); return payload(t, r) },
|
|
} {
|
|
e, _ := fn()["error"].(map[string]any)
|
|
if e["code"] != CodeForbidden || !strings.Contains(e["message"].(string), "read-only") {
|
|
t.Errorf("%s: error = %v", name, e)
|
|
}
|
|
}
|
|
|
|
res, _ := h.handleListTables(ctx, callReq(nil))
|
|
tb := payload(t, res)["tables"].([]any)[0].(map[string]any)
|
|
if ops := tb["operations"].([]any); len(ops) != 1 || ops[0] != opSelect {
|
|
t.Errorf("list_tables operations = %v", ops)
|
|
}
|
|
|
|
res, _ = h.handleDescribeTable(ctx, callReq(map[string]any{"table": "public.items"}))
|
|
p := payload(t, res)
|
|
if p["read_only"] != true {
|
|
t.Errorf("describe_table read_only = %v", p["read_only"])
|
|
}
|
|
if w, _ := p["writable_columns"].([]any); len(w) != 0 {
|
|
t.Errorf("writable_columns = %v", w)
|
|
}
|
|
|
|
cat := h.BuildCatalog()
|
|
if !cat.ReadOnly || !strings.Contains(cat.Guide, "READ-ONLY") || !strings.Contains(cat.Markdown(), "read-only") {
|
|
t.Error("catalogue must say the server is read-only")
|
|
}
|
|
for _, c := range cat.Tables[0].Columns {
|
|
if c.Writable {
|
|
t.Errorf("column %s marked writable", c.Name)
|
|
}
|
|
}
|
|
if !strings.Contains(guideFor(true, false), "READ-ONLY") || strings.Contains(guideFor(false, false), "READ-ONLY") {
|
|
t.Error("guideFor")
|
|
}
|
|
}
|
|
|
|
func newFnHandler(t *testing.T, cfg Config) *Handler {
|
|
t.Helper()
|
|
h := NewHandler(database.NewPgSQLAdapter(nil), modelregistry.NewModelRegistry(), cfg)
|
|
for _, name := range []string{"alpha", "beta"} {
|
|
name := name
|
|
err := h.RegisterFunction(Function{Name: name, Handler: func(context.Context, common.Database, map[string]any) (any, error) {
|
|
return name, nil
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return h
|
|
}
|
|
|
|
func TestReadOnlyAllowFunctionCalls(t *testing.T) {
|
|
h := newFnHandler(t, Config{AllowFunctionCalls: true})
|
|
tools := h.mcpServer.ListTools()
|
|
if tools["list_functions"] == nil || tools["call_function"] == nil {
|
|
t.Error("function tools must be registered")
|
|
}
|
|
if tools["insert_into_table"] != nil || tools["update_table"] != nil {
|
|
t.Error("write tools must stay off")
|
|
}
|
|
if g := guideFor(true, true); !strings.Contains(g, "READ-ONLY") || !strings.Contains(g, "call_function") {
|
|
t.Error("guide must mention functions")
|
|
}
|
|
if h.mcpServer.ListTools()["call_function"] == nil {
|
|
t.Error("call_function missing")
|
|
}
|
|
}
|
|
|
|
func TestAllowedFunctions(t *testing.T) {
|
|
ctx := context.Background()
|
|
for name, tc := range map[string]struct {
|
|
allowed []string
|
|
visible []string
|
|
}{
|
|
"empty allows all": {nil, []string{"alpha", "beta"}},
|
|
"only listed": {[]string{"beta"}, []string{"beta"}},
|
|
"unknown name": {[]string{"zzz"}, nil},
|
|
} {
|
|
h := newFnHandler(t, Config{AllowedFunctions: tc.allowed})
|
|
var got []string
|
|
for _, f := range h.visibleFunctions(ctx) {
|
|
got = append(got, f.Name)
|
|
}
|
|
if strings.Join(got, ",") != strings.Join(tc.visible, ",") {
|
|
t.Errorf("%s: visible = %v, want %v", name, got, tc.visible)
|
|
}
|
|
for _, fn := range []string{"alpha", "beta"} {
|
|
listed := false
|
|
for _, v := range tc.visible {
|
|
listed = listed || v == fn
|
|
}
|
|
if h.functionAllowed(fn) != listed {
|
|
t.Errorf("%s: functionAllowed(%s) = %v, want %v", name, fn, !listed, listed)
|
|
}
|
|
if !listed {
|
|
// refused before any database work, and indistinguishable from a missing function
|
|
if _, err := h.executeCall(ctx, fn, nil); err == nil || !strings.Contains(err.Error(), "unknown function") {
|
|
t.Errorf("%s: %s must be reported unknown, err=%v", name, fn, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadOnlyDefaultsOnAndCanBeDisabled(t *testing.T) {
|
|
if !(Config{}).withDefaults().readOnly {
|
|
t.Error("ReadOnly must default to on")
|
|
}
|
|
if !(Config{ReadOnly: Bool(true)}).withDefaults().readOnly {
|
|
t.Error("explicit true")
|
|
}
|
|
if (Config{ReadOnly: Bool(false)}).withDefaults().readOnly {
|
|
t.Error("Bool(false) must enable writes")
|
|
}
|
|
h := NewHandler(database.NewPgSQLAdapter(nil), modelregistry.NewModelRegistry(), Config{ReadOnly: Bool(false)})
|
|
if h.mcpServer.ListTools()["insert_into_table"] == nil {
|
|
t.Error("write tools must register when ReadOnly is Bool(false)")
|
|
}
|
|
if strings.Contains(h.BuildCatalog().Guide, "READ-ONLY") {
|
|
t.Error("guide must not claim read-only")
|
|
}
|
|
}
|