@@ -5,13 +5,136 @@ package optimizer
55
66import (
77 "context"
8+ "fmt"
89 "testing"
910
1011 "github.com/mark3labs/mcp-go/mcp"
1112 "github.com/mark3labs/mcp-go/server"
1213 "github.com/stretchr/testify/require"
1314)
1415
16+ // mockToolStore implements ToolStore for testing optimizer logic against a
17+ // controllable store without any database dependency.
18+ type mockToolStore struct {
19+ upsertFunc func (ctx context.Context , tools []server.ServerTool ) error
20+ searchFunc func (ctx context.Context , query string , allowedTools []string ) ([]ToolMatch , error )
21+ }
22+
23+ func (m * mockToolStore ) UpsertTools (ctx context.Context , tools []server.ServerTool ) error {
24+ if m .upsertFunc != nil {
25+ return m .upsertFunc (ctx , tools )
26+ }
27+ panic ("mockToolStore.UpsertTools called but not configured" )
28+ }
29+
30+ func (m * mockToolStore ) Search (ctx context.Context , query string , allowedTools []string ) ([]ToolMatch , error ) {
31+ if m .searchFunc != nil {
32+ return m .searchFunc (ctx , query , allowedTools )
33+ }
34+ panic ("mockToolStore.Search called but not configured" )
35+ }
36+
37+ func (* mockToolStore ) Close () error {
38+ return nil
39+ }
40+
41+ // TestDummyOptimizer_MockStore tests the optimizer against a mock ToolStore,
42+ // verifying search delegation, scoping, and error handling without any database.
43+ func TestDummyOptimizer_MockStore (t * testing.T ) {
44+ t .Parallel ()
45+
46+ tests := []struct {
47+ name string
48+ tools []server.ServerTool
49+ searchFunc func (ctx context.Context , query string , allowedTools []string ) ([]ToolMatch , error )
50+ upsertFunc func (ctx context.Context , tools []server.ServerTool ) error
51+ input FindToolInput
52+ expectedNames []string
53+ expectErr bool
54+ errContains string
55+ expectCreate bool // if false, expect NewDummyOptimizer to fail
56+ createErr string
57+ }{
58+ {
59+ name : "delegates search to store with allowedTools" ,
60+ tools : []server.ServerTool {
61+ {Tool : mcp.Tool {Name : "tool_a" , Description : "Tool A" }},
62+ {Tool : mcp.Tool {Name : "tool_b" , Description : "Tool B" }},
63+ },
64+ upsertFunc : func (_ context.Context , _ []server.ServerTool ) error { return nil },
65+ searchFunc : func (_ context.Context , query string , allowedTools []string ) ([]ToolMatch , error ) {
66+ require .Equal (t , "query" , query )
67+ require .ElementsMatch (t , []string {"tool_a" , "tool_b" }, allowedTools )
68+ return []ToolMatch {
69+ {Name : "tool_a" , Description : "Tool A" , Score : 0.9 },
70+ }, nil
71+ },
72+ input : FindToolInput {ToolDescription : "query" },
73+ expectedNames : []string {"tool_a" },
74+ expectCreate : true ,
75+ },
76+ {
77+ name : "propagates store search errors" ,
78+ tools : []server.ServerTool {
79+ {Tool : mcp.Tool {Name : "tool_a" , Description : "Tool A" }},
80+ },
81+ upsertFunc : func (_ context.Context , _ []server.ServerTool ) error { return nil },
82+ searchFunc : func (context.Context , string , []string ) ([]ToolMatch , error ) {
83+ return nil , fmt .Errorf ("store unavailable" )
84+ },
85+ input : FindToolInput {ToolDescription : "query" },
86+ expectErr : true ,
87+ errContains : "tool search failed" ,
88+ expectCreate : true ,
89+ },
90+ {
91+ name : "propagates store upsert errors at creation" ,
92+ tools : []server.ServerTool {
93+ {Tool : mcp.Tool {Name : "tool_a" , Description : "Tool A" }},
94+ },
95+ upsertFunc : func (context.Context , []server.ServerTool ) error {
96+ return fmt .Errorf ("upsert failed" )
97+ },
98+ input : FindToolInput {ToolDescription : "query" },
99+ expectCreate : false ,
100+ createErr : "failed to upsert tools into store" ,
101+ },
102+ }
103+
104+ for _ , tc := range tests {
105+ t .Run (tc .name , func (t * testing.T ) {
106+ t .Parallel ()
107+
108+ store := & mockToolStore {
109+ upsertFunc : tc .upsertFunc ,
110+ searchFunc : tc .searchFunc ,
111+ }
112+
113+ opt , err := NewDummyOptimizer (context .Background (), store , tc .tools )
114+ if ! tc .expectCreate {
115+ require .Error (t , err )
116+ require .Contains (t , err .Error (), tc .createErr )
117+ return
118+ }
119+ require .NoError (t , err )
120+
121+ result , err := opt .FindTool (context .Background (), tc .input )
122+ if tc .expectErr {
123+ require .Error (t , err )
124+ require .Contains (t , err .Error (), tc .errContains )
125+ return
126+ }
127+
128+ require .NoError (t , err )
129+ var names []string
130+ for _ , m := range result .Tools {
131+ names = append (names , m .Name )
132+ }
133+ require .ElementsMatch (t , tc .expectedNames , names )
134+ })
135+ }
136+ }
137+
15138func TestDummyOptimizer_FindTool (t * testing.T ) {
16139 t .Parallel ()
17140
@@ -139,7 +262,7 @@ func TestDummyOptimizerFactory_SharedStorage(t *testing.T) {
139262 require .Len (t , result2 .Tools , 1 )
140263 require .Equal (t , "tool_b" , result2 .Tools [0 ].Name )
141264
142- // Both tools exist in the shared store — verify by creating an optimizer with both in scope
265+ // Both tools exist in the shared store — verify by creating an optimizer with both in allowedTools
143266 opt3 , err := factory (ctx , []server.ServerTool {
144267 {Tool : mcp.Tool {Name : "tool_a" , Description : "Alpha tool" }},
145268 {Tool : mcp.Tool {Name : "tool_b" , Description : "Beta tool" }},
@@ -154,6 +277,80 @@ func TestDummyOptimizerFactory_SharedStorage(t *testing.T) {
154277 require .ElementsMatch (t , []string {"tool_a" , "tool_b" }, names )
155278}
156279
280+ func TestNewDummyOptimizerFactoryWithStore (t * testing.T ) {
281+ t .Parallel ()
282+
283+ tests := []struct {
284+ name string
285+ sessionATools []server.ServerTool
286+ sessionBTools []server.ServerTool
287+ searchQuery string
288+ sessionAExpect []string
289+ sessionBExpect []string
290+ }{
291+ {
292+ name : "separate sessions see only their own tools" ,
293+ sessionATools : []server.ServerTool {
294+ {Tool : mcp.Tool {Name : "tool_alpha" , Description : "Alpha tool" }},
295+ },
296+ sessionBTools : []server.ServerTool {
297+ {Tool : mcp.Tool {Name : "tool_beta" , Description : "Beta tool" }},
298+ },
299+ searchQuery : "tool" ,
300+ sessionAExpect : []string {"tool_alpha" },
301+ sessionBExpect : []string {"tool_beta" },
302+ },
303+ {
304+ name : "overlapping tools are shared" ,
305+ sessionATools : []server.ServerTool {
306+ {Tool : mcp.Tool {Name : "shared_tool" , Description : "Shared tool" }},
307+ {Tool : mcp.Tool {Name : "tool_a_only" , Description : "A only" }},
308+ },
309+ sessionBTools : []server.ServerTool {
310+ {Tool : mcp.Tool {Name : "shared_tool" , Description : "Shared tool" }},
311+ {Tool : mcp.Tool {Name : "tool_b_only" , Description : "B only" }},
312+ },
313+ searchQuery : "tool" ,
314+ sessionAExpect : []string {"shared_tool" , "tool_a_only" },
315+ sessionBExpect : []string {"shared_tool" , "tool_b_only" },
316+ },
317+ }
318+
319+ for _ , tc := range tests {
320+ t .Run (tc .name , func (t * testing.T ) {
321+ t .Parallel ()
322+
323+ store := NewInMemoryToolStore ()
324+ factory := NewDummyOptimizerFactoryWithStore (store )
325+ ctx := context .Background ()
326+
327+ optA , err := factory (ctx , tc .sessionATools )
328+ require .NoError (t , err )
329+
330+ optB , err := factory (ctx , tc .sessionBTools )
331+ require .NoError (t , err )
332+
333+ resultA , err := optA .FindTool (ctx , FindToolInput {ToolDescription : tc .searchQuery })
334+ require .NoError (t , err )
335+
336+ var namesA []string
337+ for _ , m := range resultA .Tools {
338+ namesA = append (namesA , m .Name )
339+ }
340+ require .ElementsMatch (t , tc .sessionAExpect , namesA )
341+
342+ resultB , err := optB .FindTool (ctx , FindToolInput {ToolDescription : tc .searchQuery })
343+ require .NoError (t , err )
344+
345+ var namesB []string
346+ for _ , m := range resultB .Tools {
347+ namesB = append (namesB , m .Name )
348+ }
349+ require .ElementsMatch (t , tc .sessionBExpect , namesB )
350+ })
351+ }
352+ }
353+
157354func TestDummyOptimizer_CallTool (t * testing.T ) {
158355 t .Parallel ()
159356
0 commit comments