mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-09 14:56:27 +00:00
feat(resolvemcp): add model descriptions, usage guide and catalogue export
- modelregistry: ModelInfo (description, purpose, tags, column docs) with external JSON loader, Describer fallback and gorm/bun/comment tag support - resolvemcp: surface descriptions in list_tables and describe_table, send a usage guide as MCP server instructions, add package docs - add BuildCatalog/ExportCatalog to write a JSON or Markdown API catalogue - document the descriptions map and catalogue in the README
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package modelregistry
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type infoModel struct {
|
||||
ID int `json:"id" gorm:"primaryKey;comment:Row id"`
|
||||
Email string `json:"email" bun:"email,comment:Login address"`
|
||||
Name string `json:"name" note:"Display name"`
|
||||
Plain string `json:"plain"`
|
||||
}
|
||||
|
||||
func (infoModel) ModelDescription() string { return " From the model " }
|
||||
|
||||
func TestFieldComment(t *testing.T) {
|
||||
typ := reflect.TypeOf(infoModel{})
|
||||
want := map[string]string{"ID": "Row id", "Email": "Login address", "Name": "Display name", "Plain": ""}
|
||||
for field, exp := range want {
|
||||
sf, _ := typ.FieldByName(field)
|
||||
if got := FieldComment(sf); got != exp {
|
||||
t.Errorf("%s = %q, want %q", field, got, exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelInfoPrecedence(t *testing.T) {
|
||||
r := NewModelRegistry()
|
||||
if err := r.RegisterModel("public.items", infoModel{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := r.ResolveModelInfo("public.items").Description; got != "From the model" {
|
||||
t.Errorf("describer fallback = %q", got)
|
||||
}
|
||||
|
||||
n, err := r.LoadModelInfo(strings.NewReader(
|
||||
`{"public.items":{"description":"From file","tags":["a"],"columns":{"email":"Mail"}},"public.later":{"purpose":"p"}}`))
|
||||
if err != nil || n != 2 {
|
||||
t.Fatalf("load n=%d err=%v", n, err)
|
||||
}
|
||||
info := r.ResolveModelInfo("public.items")
|
||||
if info.Description != "From file" || info.Columns["email"] != "Mail" || len(info.Tags) != 1 {
|
||||
t.Errorf("file info = %+v", info)
|
||||
}
|
||||
if _, ok := r.GetModelInfo("public.later"); !ok {
|
||||
t.Error("info for a not-yet-registered model must be kept")
|
||||
}
|
||||
|
||||
// returned info is a copy
|
||||
info.Columns["email"] = "changed"
|
||||
if got, _ := r.GetModelInfo("public.items"); got.Columns["email"] != "Mail" {
|
||||
t.Error("GetModelInfo leaked internal map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelInfoRejectsBadInput(t *testing.T) {
|
||||
r := NewModelRegistry()
|
||||
for _, in := range []string{`not json`, `{"a":{"descripton":"typo"}}`} {
|
||||
if _, err := r.LoadModelInfo(strings.NewReader(in)); err == nil {
|
||||
t.Errorf("expected error for %q", in)
|
||||
}
|
||||
}
|
||||
if _, err := r.LoadModelInfoFile("/nonexistent/x.json"); err == nil {
|
||||
t.Error("expected error for missing file")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user