返回 DeepSeek-Reasonix
tool_list_changed_schema_test.go
根目录 / internal / plugin / tool_list_changed_schema_test.go
1 package plugin
2
3 import (
4 "context"
5 "encoding/json"
6 "sync"
7 "testing"
8 "time"
9
10 "reasonix/internal/tool"
11 )
12
13 type schemaShiftTransport struct {
14 mu sync.Mutex
15 listCalls int
16 schemaType string
17 listStarted chan int
18 listRelease chan struct{}
19 notifications notificationRouter
20 }
21
22 func (t *schemaShiftTransport) call(ctx context.Context, method string, _ any) (json.RawMessage, error) {
23 if method != "tools/list" {
24 return json.RawMessage(`{}`), nil
25 }
26 t.mu.Lock()
27 t.listCalls++
28 call := t.listCalls
29 schemaType := t.schemaType
30 t.mu.Unlock()
31 select {
32 case t.listStarted <- call:
33 default:
34 }
35 select {
36 case <-t.listRelease:
37 default:
38 }
39 _ = ctx
40 response, _ := json.Marshal(map[string]any{"tools": []map[string]any{{
41 "name": "search", "description": "search",
42 "inputSchema": map[string]any{"type": "object", "properties": map[string]any{"q": map[string]any{"type": schemaType}}},
43 "annotations": map[string]any{"readOnlyHint": true},
44 }}})
45 return response, nil
46 }
47
48 func (*schemaShiftTransport) close() {}
49 func (t *schemaShiftTransport) registerNotification(method string, callback func(json.RawMessage)) func() {
50 return t.notifications.registerNotification(method, callback)
51 }
52
53 func TestListChangedInvalidatesValidatorAndGeneration(t *testing.T) {
54 tr := &schemaShiftTransport{
55 schemaType: "string",
56 listStarted: make(chan int, 8),
57 listRelease: make(chan struct{}, 8),
58 }
59 ctx, cancel := context.WithCancel(context.Background())
60 defer cancel()
61 client := &Client{
62 name: "shift", t: tr, spec: Spec{Name: "shift"}, capabilities: clientCapabilities{toolsListChanged: true},
63 refresh: toolListRefreshState{ctx: ctx, cancel: cancel, wait: func(context.Context, time.Duration) error { return nil }},
64 }
65 tools, err := client.listTools(ctx)
66 if err != nil {
67 t.Fatalf("initial list: %v", err)
68 }
69 adapter := tools[0]
70 firstGen := adapter.(*remoteTool).generation
71 got := tool.ValidateArguments(adapter, json.RawMessage(`{"q":"ok"}`))
72 if got.CompileErr != nil || len(got.Violations) != 0 {
73 t.Fatalf("initial validate: %+v", got)
74 }
75 oldFP := tool.SchemaFingerprint(adapter.Schema())
76
77 tr.mu.Lock()
78 tr.schemaType = "number"
79 tr.mu.Unlock()
80 releaseWait := make(chan struct{})
81 client.refresh.mu.Lock()
82 client.refresh.wait = func(ctx context.Context, _ time.Duration) error {
83 select {
84 case <-releaseWait:
85 return nil
86 case <-ctx.Done():
87 return ctx.Err()
88 }
89 }
90 client.refresh.mu.Unlock()
91 client.watchToolListChanges()
92 tr.notifications.dispatchNotification("notifications/tools/list_changed", nil)
93 done := refreshDone(t, client)
94 close(releaseWait)
95 waitClosed(t, done, "schema refresh")
96
97 tools, ok := client.cachedTools()
98 if !ok || len(tools) == 0 {
99 t.Fatal("refreshed catalog missing")
100 }
101 adapter = tools[0]
102 if adapter.(*remoteTool).generation == firstGen {
103 t.Fatal("listChanged did not bump catalog generation")
104 }
105 newFP := tool.SchemaFingerprint(adapter.Schema())
106 if newFP == oldFP {
107 t.Fatal("schema fingerprint did not change")
108 }
109 got = tool.ValidateArguments(adapter, json.RawMessage(`{"q":"ok"}`))
110 if len(got.Violations) == 0 {
111 t.Fatal("new number schema accepted a string")
112 }
113 }
114
114 lines GO