- 新增依赖:github.com/modelcontextprotocol/go-sdk v1.8.0(direct)+ github.com/google/jsonschema-go v0.4.3 (SDK 自带依赖,直接用于 schema 校验/默认值,避免再造一份);go.sum 纯新增 - 删除自研协议层:rpc.go / stdio.go / http.go / sse.go 及其测试 - 用 SDK 的成熟逻辑:命令传输(stdio 子进程)/ SSE / Streamable HTTP、协议版本协商与 -32022 回退、keepalive(含失败阈值)、tools/list_changed 订阅、能力协商(InitializeResult.Capabilities) - 保留自研的四件事(其余零自研):配置→传输、发现→registry 注册、能力族推断、schema 缓存(Prepare 命中不重复解析) - 能力闸诚实化:服务器声明 capabilities 但不含 tools 时 ListTools/DiscoverTools 返回 ErrCapabilityNotDeclared(不静默空集);完全不声明 capabilities 的老式服务器仍按 legacy 处理 - 复核:build 绿 + capability 全包测试绿(mcp 42 用例,含真子进程 stdio / httptest Streamable HTTP / 内存传输三类假 server,不依赖外网)+ `-race` 干净;变异 2 条(featuresFrom 恒 true / 删 applySchema 调用)已还原 - 无 REST/WS/env/数据目录变更;包内 API 有增删(见回报),仓外调用方签名未变(未改 cmd/cyrene 与 plugin/manager)
150 lines
4.7 KiB
Go
150 lines
4.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestSchemaCache_PutGetInvalidate 覆盖缓存基本语义与按服务器失效。
|
|
func TestSchemaCache_PutGetInvalidate(t *testing.T) {
|
|
t.Parallel()
|
|
cache := NewSchemaCache()
|
|
schema := map[string]any{"type": "object", "properties": map[string]any{"a": map[string]any{"type": "number"}}}
|
|
if err := cache.Put("s1", "tool", schema); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
if err := cache.Put("s2", "tool", schema); err != nil {
|
|
t.Fatalf("Put(s2): %v", err)
|
|
}
|
|
if _, ok := cache.Get("s1", "tool"); !ok {
|
|
t.Fatal("Get(s1,tool) 未命中")
|
|
}
|
|
if cache.Len() != 2 {
|
|
t.Fatalf("Len = %d, 期望 2", cache.Len())
|
|
}
|
|
|
|
// 只失效 s1,s2 的同名工具必须留下。
|
|
cache.Invalidate("s1")
|
|
if _, ok := cache.Get("s1", "tool"); ok {
|
|
t.Fatal("Invalidate(s1) 后仍然命中")
|
|
}
|
|
if _, ok := cache.Get("s2", "tool"); !ok {
|
|
t.Fatal("Invalidate(s1) 误删了 s2 的缓存")
|
|
}
|
|
|
|
cache.Clear()
|
|
if cache.Len() != 0 {
|
|
t.Fatalf("Clear 后 Len = %d, 期望 0", cache.Len())
|
|
}
|
|
}
|
|
|
|
// TestSchemaCache_Prepare_AppliesDefaultsAndValidates 命中缓存时补默认值并校验。
|
|
func TestSchemaCache_Prepare_AppliesDefaultsAndValidates(t *testing.T) {
|
|
t.Parallel()
|
|
cache := NewSchemaCache()
|
|
schema := map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]any{"type": "string", "default": "world"},
|
|
"n": map[string]any{"type": "number"},
|
|
},
|
|
"required": []string{"n"},
|
|
}
|
|
if err := cache.Put("s", "t", schema); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
out, err := cache.Prepare("s", "t", nil, map[string]any{"n": 1})
|
|
if err != nil {
|
|
t.Fatalf("Prepare: %v", err)
|
|
}
|
|
if out["name"] != "world" {
|
|
t.Fatalf("默认值未生效:%v", out)
|
|
}
|
|
|
|
if _, err := cache.Prepare("s", "t", nil, map[string]any{"n": "x"}); err == nil {
|
|
t.Fatal("类型错误的参数没有被拦下")
|
|
}
|
|
if _, err := cache.Prepare("s", "t", nil, map[string]any{}); err == nil {
|
|
t.Fatal("缺必填参数没有被拦下")
|
|
}
|
|
}
|
|
|
|
// TestSchemaCache_Prepare_HitsCacheWithoutReparsing 证明「命中缓存」这条要求:
|
|
// 发现阶段已缓存 → 后续调用全是命中,未命中计数为 0(没有重复拉取/解析)。
|
|
func TestSchemaCache_Prepare_HitsCacheWithoutReparsing(t *testing.T) {
|
|
t.Parallel()
|
|
cache := NewSchemaCache()
|
|
if err := cache.Put("s", "t", emptyObjectSchema()); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if _, err := cache.Prepare("s", "t", nil, map[string]any{}); err != nil {
|
|
t.Fatalf("Prepare #%d: %v", i, err)
|
|
}
|
|
}
|
|
hits, misses := cache.Stats()
|
|
if hits != 3 || misses != 0 {
|
|
t.Fatalf("Stats = (%d hits, %d misses), 期望 (3, 0)", hits, misses)
|
|
}
|
|
}
|
|
|
|
// TestSchemaCache_Prepare_MissFillsFromFallback 缓存未命中时用发现结果兜底填入,
|
|
// 且失效后第一次调用计一次未命中。
|
|
func TestSchemaCache_Prepare_MissFillsFromFallback(t *testing.T) {
|
|
t.Parallel()
|
|
cache := NewSchemaCache()
|
|
fallback := map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"name": map[string]any{"type": "string", "default": "fallback"},
|
|
},
|
|
}
|
|
out, err := cache.Prepare("s", "t", fallback, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Prepare: %v", err)
|
|
}
|
|
if out["name"] != "fallback" {
|
|
t.Fatalf("兜底 schema 的默认值未生效:%v", out)
|
|
}
|
|
if _, misses := cache.Stats(); misses != 1 {
|
|
t.Fatalf("misses = %d, 期望 1", misses)
|
|
}
|
|
if cache.Len() != 1 {
|
|
t.Fatalf("未命中后没有回填缓存:Len = %d", cache.Len())
|
|
}
|
|
|
|
cache.Invalidate("s")
|
|
if _, err := cache.Prepare("s", "t", nil, map[string]any{"x": 1}); err != nil {
|
|
t.Fatalf("无 schema 时应降级为不校验:%v", err)
|
|
}
|
|
}
|
|
|
|
// TestSchemaCache_Put_BadSchemaDegrades 无法解析的 schema 只降级为「不校验」,不阻断调用。
|
|
func TestSchemaCache_Put_BadSchemaDegrades(t *testing.T) {
|
|
t.Parallel()
|
|
cache := NewSchemaCache()
|
|
bad := map[string]any{"type": "object", "properties": map[string]any{"a": map[string]any{"$ref": "#/$defs/does-not-exist"}}}
|
|
err := cache.Put("s", "t", bad)
|
|
if err == nil {
|
|
t.Fatal("病态 schema 未报错,期望解析失败可见")
|
|
}
|
|
if _, ok := cache.Get("s", "t"); !ok {
|
|
t.Fatal("解析失败时原始 schema 没有缓存下来(应降级保留)")
|
|
}
|
|
if _, err := cache.Prepare("s", "t", nil, map[string]any{"whatever": true}); err != nil {
|
|
t.Fatalf("降级路径不应报错:%v", err)
|
|
}
|
|
}
|
|
|
|
// TestSchemaCache_Prepare_NilSchemaSafe nil schema 走空对象兜底,不 panic。
|
|
func TestSchemaCache_Prepare_NilSchemaSafe(t *testing.T) {
|
|
t.Parallel()
|
|
cache := NewSchemaCache()
|
|
if err := cache.Put("s", "t", nil); err != nil {
|
|
t.Fatalf("Put(nil): %v", err)
|
|
}
|
|
if _, err := cache.Prepare("s", "t", nil, nil); err != nil {
|
|
t.Fatalf("Prepare(nil args): %v", err)
|
|
}
|
|
}
|