返回 DeepSeek-Reasonix
usecapability_live_catalog_test.go
根目录 / internal / agent / usecapability_live_catalog_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/capability"
13 "reasonix/internal/plugin"
14 "reasonix/internal/tool"
15 )
16
17 func twoToolMCPServer(t *testing.T) *httptest.Server {
18 t.Helper()
19 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20 var request struct {
21 ID *int `json:"id"`
22 Method string `json:"method"`
23 }
24 if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
25 http.Error(w, "bad request", http.StatusBadRequest)
26 return
27 }
28 if request.ID == nil {
29 w.WriteHeader(http.StatusAccepted)
30 return
31 }
32 var result any
33 switch request.Method {
34 case "initialize":
35 result = map[string]any{"protocolVersion": "2024-11-05", "serverInfo": map[string]any{"name": "bfexplorer", "version": "1"}}
36 case "tools/list":
37 result = map[string]any{"tools": []map[string]any{
38 {
39 "name": "get_data", "description": "get data",
40 "inputSchema": map[string]any{"type": "object"},
41 },
42 {
43 "name": "execute_console_script", "description": "execute a console script",
44 "inputSchema": map[string]any{"type": "object", "properties": map[string]any{"filePathName": map[string]any{"type": "string"}}},
45 },
46 }}
47 case "tools/call":
48 result = map[string]any{"content": []map[string]any{{"type": "text", "text": "ok"}}}
49 }
50 w.Header().Set("Content-Type", "application/json")
51 _ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": *request.ID, "result": result})
52 }))
53 }
54
55 // TestCatalogSurfacesLiveServerToolMissingFromRegistrySnapshot reproduces
56 // issue #9516: the session registry holds a stale subset of an MCP server's
57 // tools, the live server exposes one more, and the capability catalog must
58 // still surface the live tool so inspect on its mcp-tool id succeeds instead
59 // of returning "unknown capability_id".
60 func TestCatalogSurfacesLiveServerToolMissingFromRegistrySnapshot(t *testing.T) {
61 t.Setenv("REASONIX_CACHE_HOME", t.TempDir())
62 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
63 defer cancel()
64
65 server := twoToolMCPServer(t)
66 defer server.Close()
67
68 spec := plugin.Spec{Name: "bfexplorer", Type: "http", URL: server.URL, Authorized: true}
69 host := plugin.NewHost()
70 defer host.Close()
71
72 // Stale registry snapshot: only get_data was known when it was registered.
73 reg := tool.NewRegistry()
74 reg.Add(fakeTool{name: plugin.ModelToolName("bfexplorer", "get_data"), readOnly: true})
75
76 // Catalog assembly mirrors boot.Build: registry contract entries plus the
77 // runtime's configured/cached/live MCP state.
78 var runtime *MCPCapabilityRuntime
79 catalogFn := func() capability.Catalog {
80 conn := map[string]bool{}
81 for _, name := range host.ServerNames() {
82 conn[name] = true
83 }
84 opts := capability.CatalogOptions{Tools: reg.AllContractEntries(), Connected: conn}
85 if runtime != nil {
86 opts.Plugins, opts.CachedTools, opts.CacheKeyOK, opts.Disabled, opts.ProxyTools = runtime.CapabilityCatalogState()
87 }
88 return capability.BuildCatalog(opts)
89 }
90 runtime = NewMCPCapabilityRuntime(ctx, host, []plugin.Spec{spec}, reg, catalogFn)
91
92 // The server connects after the runtime exists and never sends
93 // notifications/tools/list_changed, so only the live tools/list — not the
94 // registry — knows about execute_console_script.
95 if _, err := host.Add(ctx, spec); err != nil {
96 t.Fatal(err)
97 }
98
99 frontend := runtime.NewFrontend(capability.NewLedger(), nil)
100 listing, err := frontend.Execute(ctx, json.RawMessage(`{"action":"call","capability_id":"mcp-server:bfexplorer"}`))
101 if err != nil {
102 t.Fatalf("connect-and-list on the ready server: %v", err)
103 }
104 if !strings.Contains(listing, "execute_console_script") {
105 t.Fatalf("live server directory must list execute_console_script:\n%s", listing)
106 }
107
108 out, err := frontend.Execute(ctx, json.RawMessage(`{"action":"inspect","capability_id":"mcp-tool:bfexplorer/execute_console_script"}`))
109 if err != nil {
110 t.Fatalf("inspect of a tool the live connected server exposes must succeed, got: %v", err)
111 }
112 if !strings.Contains(out, "mcp-tool:bfexplorer/execute_console_script") {
113 t.Fatalf("inspect payload lost the tool identity:\n%s", out)
114 }
115 }
116
116 lines GO