blob: b80915a72da0fd460a39c925225f3cfffc70dd3c [file] [log] [blame]
Akronb1c71e62025-06-12 16:08:54 +02001package tools
2
3import (
Akron8138c352025-06-12 16:34:42 +02004 "context"
Akronb1c71e62025-06-12 16:08:54 +02005 "testing"
6
7 "github.com/korap/korap-mcp/service"
Akron8138c352025-06-12 16:34:42 +02008 "github.com/mark3labs/mcp-go/mcp"
Akronb1c71e62025-06-12 16:08:54 +02009 "github.com/stretchr/testify/assert"
10)
11
12func TestSearchTool_Name(t *testing.T) {
13 client := &service.Client{}
14 tool := NewSearchTool(client)
15
16 assert.Equal(t, "korap_search", tool.Name())
17}
18
19func TestSearchTool_Description(t *testing.T) {
20 client := &service.Client{}
21 tool := NewSearchTool(client)
22
23 expected := "Search for words or phrases in KorAP corpora using various query languages"
24 assert.Equal(t, expected, tool.Description())
25}
26
27func TestSearchTool_InputSchema(t *testing.T) {
28 client := &service.Client{}
29 tool := NewSearchTool(client)
30
31 schema := tool.InputSchema()
32
33 // Verify it's an object type
34 assert.Equal(t, "object", schema["type"])
Akron8db31c32025-06-17 12:22:41 +020035 assert.Equal(t, "KorAP Search Parameters", schema["title"])
36 assert.Contains(t, schema["description"], "Parameters for searching text corpora")
37 assert.Equal(t, false, schema["additionalProperties"])
Akronb1c71e62025-06-12 16:08:54 +020038
39 // Verify properties exist
40 properties, ok := schema["properties"].(map[string]interface{})
41 assert.True(t, ok)
42 assert.Contains(t, properties, "query")
43 assert.Contains(t, properties, "query_language")
44 assert.Contains(t, properties, "corpus")
45 assert.Contains(t, properties, "count")
Akron8db31c32025-06-17 12:22:41 +020046
47 // Verify query property details
48 query, ok := properties["query"].(map[string]interface{})
49 assert.True(t, ok)
50 assert.Equal(t, "string", query["type"])
51 assert.Contains(t, query["description"], "search query")
52 assert.Equal(t, 1, query["minLength"])
53 assert.Equal(t, 1000, query["maxLength"])
54 examples, ok := query["examples"].([]string)
55 assert.True(t, ok)
56 assert.Contains(t, examples, "Haus")
57
58 // Verify query_language property details
59 queryLang, ok := properties["query_language"].(map[string]interface{})
60 assert.True(t, ok)
61 assert.Equal(t, "string", queryLang["type"])
62 assert.Contains(t, queryLang["description"], "Query language to use for parsing")
63 enum, ok := queryLang["enum"].([]string)
64 assert.True(t, ok)
65 assert.Contains(t, enum, "poliqarp")
66 assert.Contains(t, enum, "cosmas2")
67 assert.Contains(t, enum, "annis")
68 assert.Contains(t, enum, "cql")
69 assert.Contains(t, enum, "cqp")
70 assert.Contains(t, enum, "fcsql")
71 assert.Equal(t, "poliqarp", queryLang["default"])
72
73 // Verify corpus property details
74 corpus, ok := properties["corpus"].(map[string]interface{})
75 assert.True(t, ok)
76 assert.Equal(t, "string", corpus["type"])
77 assert.Contains(t, corpus["description"], "Virtual corpus query")
78 assert.NotEmpty(t, corpus["pattern"])
79 corpusExamples, ok := corpus["examples"].([]string)
80 assert.True(t, ok)
81 assert.Contains(t, corpusExamples, "corpusSigle = \"GOE\"")
82
83 // Verify count property details
84 count, ok := properties["count"].(map[string]interface{})
85 assert.True(t, ok)
86 assert.Equal(t, "integer", count["type"])
87 assert.Contains(t, count["description"], "Maximum number")
88 assert.Equal(t, 0, count["minimum"])
89 assert.Equal(t, 10000, count["maximum"])
90 assert.Equal(t, 25, count["default"])
91 countExamples, ok := count["examples"].([]interface{})
92 assert.True(t, ok)
93 assert.Contains(t, countExamples, 25)
Akronb1c71e62025-06-12 16:08:54 +020094
95 // Verify required fields
96 required, ok := schema["required"].([]string)
97 assert.True(t, ok)
98 assert.Contains(t, required, "query")
Akron8db31c32025-06-17 12:22:41 +020099 assert.Len(t, required, 1) // Only query should be required
100}
101
102func TestSearchTool_SchemaCompliance(t *testing.T) {
103 // Test various parameter combinations against the schema
104 client := &service.Client{}
105 tool := NewSearchTool(client)
106
107 tests := []struct {
108 name string
109 arguments map[string]interface{}
110 expectValid bool
111 errorMsg string
112 }{
113 {
114 name: "valid_minimal",
115 arguments: map[string]interface{}{
116 "query": "test",
117 },
118 expectValid: true,
119 },
120 {
121 name: "valid_full",
122 arguments: map[string]interface{}{
123 "query": "word",
124 "query_language": "cosmas2",
125 "corpus": "test-corpus",
126 "count": 10,
127 },
128 expectValid: true,
129 },
130 {
131 name: "missing_required_query",
132 arguments: map[string]interface{}{
133 "query_language": "poliqarp",
134 },
135 expectValid: false,
136 errorMsg: "query parameter is required",
137 },
138 {
139 name: "invalid_query_language",
140 arguments: map[string]interface{}{
141 "query": "test",
142 "query_language": "invalid",
143 },
144 expectValid: false,
145 errorMsg: "invalid query language",
146 },
147 {
148 name: "invalid_count_negative",
149 arguments: map[string]interface{}{
150 "query": "test",
151 "count": -1,
152 },
153 expectValid: false,
154 errorMsg: "count must be",
155 },
156 {
157 name: "invalid_count_too_large",
158 arguments: map[string]interface{}{
159 "query": "test",
160 "count": 20000,
161 },
162 expectValid: false,
163 errorMsg: "count must be",
164 },
165 {
166 name: "empty_query",
167 arguments: map[string]interface{}{
168 "query": "",
169 },
170 expectValid: false,
171 errorMsg: "query is required and cannot be empty",
172 },
173 {
174 name: "count_zero_valid",
175 arguments: map[string]interface{}{
176 "query": "test",
177 "count": 0,
178 },
179 expectValid: true, // Zero count should be valid (uses default behavior)
180 },
181 }
182
183 for _, tt := range tests {
184 t.Run(tt.name, func(t *testing.T) {
185 request := mcp.CallToolRequest{
186 Params: mcp.CallToolParams{
187 Arguments: tt.arguments,
188 },
189 }
190
191 _, err := tool.Execute(context.Background(), request)
192
193 if tt.expectValid {
194 // For valid requests, we expect authentication/client errors, not validation errors
195 if err != nil {
196 assert.NotContains(t, err.Error(), "validation")
197 assert.Contains(t, err.Error(), "authentication")
198 }
199 } else {
200 // For invalid requests, we expect validation errors
201 assert.Error(t, err)
202 if tt.errorMsg != "" {
203 assert.Contains(t, err.Error(), tt.errorMsg)
204 }
205 }
206 })
207 }
Akronb1c71e62025-06-12 16:08:54 +0200208}
209
210func TestNewSearchTool(t *testing.T) {
211 client := &service.Client{}
212 tool := NewSearchTool(client)
213
214 assert.NotNil(t, tool)
215 assert.Equal(t, client, tool.client)
216}
217
Akron8138c352025-06-12 16:34:42 +0200218func TestSearchTool_Execute_MissingQuery(t *testing.T) {
219 client := &service.Client{}
220 tool := NewSearchTool(client)
Akronb1c71e62025-06-12 16:08:54 +0200221
Akron8138c352025-06-12 16:34:42 +0200222 // Create request without query parameter
223 request := mcp.CallToolRequest{
224 Params: mcp.CallToolParams{
225 Arguments: map[string]interface{}{},
226 },
227 }
228
229 _, err := tool.Execute(context.Background(), request)
230 assert.Error(t, err)
231 assert.Contains(t, err.Error(), "query parameter is required")
232}
233
234func TestSearchTool_Execute_NilClient(t *testing.T) {
235 tool := NewSearchTool(nil)
236
237 request := mcp.CallToolRequest{
238 Params: mcp.CallToolParams{
239 Arguments: map[string]interface{}{
240 "query": "test",
241 },
242 },
243 }
244
245 _, err := tool.Execute(context.Background(), request)
246 assert.Error(t, err)
247 assert.Contains(t, err.Error(), "KorAP client not configured")
248}
249
250func TestSearchTool_Execute_ParameterExtraction(t *testing.T) {
251 // This test verifies that parameters are extracted correctly
252 // It should fail with authentication error since we don't have a mock server
253 // but we can verify the parameters were parsed correctly by checking the log messages
254
255 client := &service.Client{}
256 tool := NewSearchTool(client)
257
258 tests := []struct {
259 name string
260 arguments map[string]interface{}
261 expectErr bool
262 }{
263 {
264 name: "minimal_query",
265 arguments: map[string]interface{}{
266 "query": "test",
267 },
268 expectErr: true, // Will fail at authentication
269 },
270 {
271 name: "full_parameters",
272 arguments: map[string]interface{}{
273 "query": "word",
274 "query_language": "cosmas2",
275 "corpus": "test-corpus",
276 "count": 10,
277 },
278 expectErr: true, // Will fail at authentication
279 },
280 {
281 name: "invalid_count_type",
282 arguments: map[string]interface{}{
283 "query": "test",
284 "count": "invalid", // Should use default
285 },
286 expectErr: true, // Will fail at authentication
287 },
288 }
289
290 for _, tt := range tests {
291 t.Run(tt.name, func(t *testing.T) {
292 request := mcp.CallToolRequest{
293 Params: mcp.CallToolParams{
294 Arguments: tt.arguments,
295 },
296 }
297
298 _, err := tool.Execute(context.Background(), request)
299 if tt.expectErr {
300 assert.Error(t, err)
301 }
302 })
303 }
304}
305
306func TestSearchTool_formatSearchResults(t *testing.T) {
307 client := &service.Client{}
308 tool := NewSearchTool(client)
309
310 // Test empty response
311 emptyResponse := &service.SearchResponse{
312 Query: service.SearchQuery{
313 Query: "test",
314 QueryLang: "poliqarp",
315 },
316 Meta: service.SearchMeta{
317 TotalResults: 0,
318 Count: 0,
319 StartIndex: 0,
320 },
321 Matches: []service.SearchMatch{},
322 }
323
324 result := tool.formatSearchResults(emptyResponse)
325 assert.Contains(t, result, "KorAP Search Results")
326 assert.Contains(t, result, "Query: test")
327 assert.Contains(t, result, "Query Language: poliqarp")
328 assert.Contains(t, result, "Total Results: 0")
329 assert.Contains(t, result, "No matches found")
330
331 // Test response with matches
332 responseWithMatches := &service.SearchResponse{
333 Query: service.SearchQuery{
334 Query: "word",
335 QueryLang: "poliqarp",
336 Collection: "test-corpus",
337 },
338 Meta: service.SearchMeta{
339 TotalResults: 5,
340 Count: 2,
341 StartIndex: 0,
342 SearchTime: 0.123,
343 },
344 Matches: []service.SearchMatch{
345 {
346 TextSigle: "text1",
347 Snippet: "This is a test snippet",
348 PubPlace: "Berlin",
349 MatchID: "match1",
350 Position: 10,
351 },
352 {
353 TextSigle: "text2",
354 Snippet: "Another test snippet",
355 Position: 25,
356 },
357 },
358 }
359
360 result = tool.formatSearchResults(responseWithMatches)
361 assert.Contains(t, result, "Query: word")
362 assert.Contains(t, result, "Query Language: poliqarp")
363 assert.Contains(t, result, "Corpus: test-corpus")
364 assert.Contains(t, result, "Total Results: 5")
365 assert.Contains(t, result, "Shown: 1-2")
366 assert.Contains(t, result, "Search Time: 0.123 seconds")
367 assert.Contains(t, result, "1. Text: text1")
368 assert.Contains(t, result, "Snippet: This is a test snippet")
369 assert.Contains(t, result, "Publication: Berlin")
370 assert.Contains(t, result, "2. Text: text2")
371 assert.Contains(t, result, "Position: 25")
372
373 // Test response with warnings
374 responseWithWarnings := &service.SearchResponse{
375 Query: service.SearchQuery{
376 Query: "test",
377 CutOff: true,
378 TimeExceeded: true,
379 },
380 Meta: service.SearchMeta{},
381 Matches: []service.SearchMatch{},
382 }
383
384 result = tool.formatSearchResults(responseWithWarnings)
385 assert.Contains(t, result, "Results were cut off due to limits")
386 assert.Contains(t, result, "Search time limit was exceeded")
Akronb1c71e62025-06-12 16:08:54 +0200387}