blob: f1194d0957b4095128ab9df59155d3a99173202f [file] [log] [blame]
Akronbd154ea2025-06-12 17:01:58 +02001package tools
2
3import (
4 "context"
Akron8db31c32025-06-17 12:22:41 +02005 "strings"
Akronbd154ea2025-06-12 17:01:58 +02006 "testing"
7
8 "github.com/korap/korap-mcp/service"
9 "github.com/mark3labs/mcp-go/mcp"
10 "github.com/stretchr/testify/assert"
11)
12
13func TestMetadataTool_Name(t *testing.T) {
14 client := &service.Client{}
15 tool := NewMetadataTool(client)
16
17 assert.Equal(t, "korap_metadata", tool.Name())
18}
19
20func TestMetadataTool_Description(t *testing.T) {
21 client := &service.Client{}
22 tool := NewMetadataTool(client)
23
24 expected := "Retrieve metadata and statistics for KorAP corpora"
25 assert.Equal(t, expected, tool.Description())
26}
27
28func TestMetadataTool_InputSchema(t *testing.T) {
29 client := &service.Client{}
30 tool := NewMetadataTool(client)
31
32 schema := tool.InputSchema()
33
34 // Verify it's an object type
35 assert.Equal(t, "object", schema["type"])
Akron8db31c32025-06-17 12:22:41 +020036 assert.Equal(t, "KorAP Metadata Parameters", schema["title"])
37 assert.Contains(t, schema["description"], "Parameters for retrieving corpus metadata")
38 assert.Equal(t, false, schema["additionalProperties"])
Akronbd154ea2025-06-12 17:01:58 +020039
40 // Verify properties exist
Akron708f3912025-06-17 12:26:02 +020041 properties, ok := schema["properties"].(map[string]any)
Akronbd154ea2025-06-12 17:01:58 +020042 assert.True(t, ok)
43 assert.Contains(t, properties, "action")
44 assert.Contains(t, properties, "corpus")
45
46 // Verify action property details
Akron708f3912025-06-17 12:26:02 +020047 action, ok := properties["action"].(map[string]any)
Akronbd154ea2025-06-12 17:01:58 +020048 assert.True(t, ok)
49 assert.Equal(t, "string", action["type"])
Akron8db31c32025-06-17 12:22:41 +020050 assert.Contains(t, action["description"], "Type of metadata operation")
Akronbd154ea2025-06-12 17:01:58 +020051
52 enum, ok := action["enum"].([]string)
53 assert.True(t, ok)
54 assert.Contains(t, enum, "list")
55 assert.Contains(t, enum, "statistics")
56 assert.Equal(t, "list", action["default"])
57
Akron8db31c32025-06-17 12:22:41 +020058 actionExamples, ok := action["examples"].([]string)
59 assert.True(t, ok)
60 assert.Contains(t, actionExamples, "list")
61 assert.Contains(t, actionExamples, "statistics")
62
63 // Verify corpus property details
Akron708f3912025-06-17 12:26:02 +020064 corpus, ok := properties["corpus"].(map[string]any)
Akron8db31c32025-06-17 12:22:41 +020065 assert.True(t, ok)
66 assert.Equal(t, "string", corpus["type"])
67 assert.Contains(t, corpus["description"], "Virtual corpus query")
68 assert.Contains(t, corpus["examples"], "corpusSigle = \"GOE\"")
69
Akronbd154ea2025-06-12 17:01:58 +020070 // Verify required fields
71 required, ok := schema["required"].([]string)
72 assert.True(t, ok)
73 assert.Contains(t, required, "action")
Akron8db31c32025-06-17 12:22:41 +020074 assert.Len(t, required, 1) // Only action should be required
Akronbd154ea2025-06-12 17:01:58 +020075}
76
77func TestNewMetadataTool(t *testing.T) {
78 client := &service.Client{}
79 tool := NewMetadataTool(client)
80
81 assert.NotNil(t, tool)
82 assert.Equal(t, client, tool.client)
83}
84
85func TestMetadataTool_Execute_MissingAction(t *testing.T) {
86 client := &service.Client{}
87 tool := NewMetadataTool(client)
88
89 // Create request without action parameter
90 request := mcp.CallToolRequest{
91 Params: mcp.CallToolParams{
Akron708f3912025-06-17 12:26:02 +020092 Arguments: map[string]any{},
Akronbd154ea2025-06-12 17:01:58 +020093 },
94 }
95
96 _, err := tool.Execute(context.Background(), request)
97 assert.Error(t, err)
98 assert.Contains(t, err.Error(), "action parameter is required")
99}
100
101func TestMetadataTool_Execute_NilClient(t *testing.T) {
102 tool := NewMetadataTool(nil)
103
104 request := mcp.CallToolRequest{
105 Params: mcp.CallToolParams{
Akron708f3912025-06-17 12:26:02 +0200106 Arguments: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200107 "action": "list",
108 },
109 },
110 }
111
112 _, err := tool.Execute(context.Background(), request)
113 assert.Error(t, err)
114 assert.Contains(t, err.Error(), "KorAP client not configured")
115}
116
117func TestMetadataTool_Execute_UnknownAction(t *testing.T) {
118 client := &service.Client{}
119 tool := NewMetadataTool(client)
120
121 request := mcp.CallToolRequest{
122 Params: mcp.CallToolParams{
Akron708f3912025-06-17 12:26:02 +0200123 Arguments: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200124 "action": "unknown",
125 },
126 },
127 }
128
129 _, err := tool.Execute(context.Background(), request)
130 assert.Error(t, err)
Akron81f709c2025-06-12 17:30:55 +0200131 // The validation error should come before authentication
132 assert.Contains(t, err.Error(), "invalid metadata request")
133 assert.Contains(t, err.Error(), "invalid action")
Akronbd154ea2025-06-12 17:01:58 +0200134}
135
136func TestMetadataTool_Execute_StatisticsWithoutCorpus(t *testing.T) {
137 tool := NewMetadataTool(nil)
138
139 request := mcp.CallToolRequest{
140 Params: mcp.CallToolParams{
Akron708f3912025-06-17 12:26:02 +0200141 Arguments: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200142 "action": "statistics",
143 },
144 },
145 }
146
147 _, err := tool.Execute(context.Background(), request)
148 assert.Error(t, err)
149 // Should fail at client check since corpus is now optional
150 assert.Contains(t, err.Error(), "KorAP client not configured")
151}
152
153func TestMetadataTool_Execute_ParameterExtraction(t *testing.T) {
154 // This test verifies that parameters are extracted correctly
155 // It should fail with authentication error since we don't have a mock server
156 // but we can verify the parameters were parsed correctly by checking the log messages
157
158 client := &service.Client{}
159 tool := NewMetadataTool(client)
160
161 tests := []struct {
162 name string
Akron708f3912025-06-17 12:26:02 +0200163 arguments map[string]any
Akronbd154ea2025-06-12 17:01:58 +0200164 expectErr bool
165 }{
166 {
167 name: "list_action",
Akron708f3912025-06-17 12:26:02 +0200168 arguments: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200169 "action": "list",
170 },
171 expectErr: true, // Will fail at authentication
172 },
173 {
174 name: "statistics_action",
Akron708f3912025-06-17 12:26:02 +0200175 arguments: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200176 "action": "statistics",
177 "corpus": "test-corpus",
178 },
179 expectErr: true, // Will fail at authentication
180 },
181 {
182 name: "statistics_with_empty_corpus",
Akron708f3912025-06-17 12:26:02 +0200183 arguments: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200184 "action": "statistics",
185 "corpus": "",
186 },
187 expectErr: true, // Will fail at authentication (corpus is optional)
188 },
189 }
190
191 for _, tt := range tests {
192 t.Run(tt.name, func(t *testing.T) {
193 request := mcp.CallToolRequest{
194 Params: mcp.CallToolParams{
195 Arguments: tt.arguments,
196 },
197 }
198
199 _, err := tool.Execute(context.Background(), request)
200 if tt.expectErr {
201 assert.Error(t, err)
202 }
203 })
204 }
205}
206
207func TestMetadataTool_formatCorpusList(t *testing.T) {
208 client := &service.Client{}
209 tool := NewMetadataTool(client)
210
211 // Test empty response
212 emptyResponse := &service.CorpusListResponse{
213 Corpora: []service.CorpusInfo{},
214 }
215
216 result := tool.formatCorpusList(emptyResponse)
217 assert.Contains(t, result, "KorAP Available Corpora")
218 assert.Contains(t, result, "No corpora available")
219
220 // Test response with corpora
221 responseWithCorpora := &service.CorpusListResponse{
222 Corpora: []service.CorpusInfo{
223 {
224 ID: "corpus1",
225 Name: "Test Corpus 1",
226 Description: "A test corpus",
227 Documents: 100,
228 Tokens: 50000,
229 Sentences: 2500,
230 Paragraphs: 500,
231 },
232 {
233 ID: "corpus2",
234 Name: "Test Corpus 2",
235 Documents: 200,
236 Tokens: 75000,
237 },
238 },
239 }
240
241 result = tool.formatCorpusList(responseWithCorpora)
242 assert.Contains(t, result, "KorAP Available Corpora")
243 assert.Contains(t, result, "Total Corpora: 2")
244 assert.Contains(t, result, "1. Test Corpus 1")
245 assert.Contains(t, result, "ID: corpus1")
246 assert.Contains(t, result, "Description: A test corpus")
247 assert.Contains(t, result, "Documents: 100")
248 assert.Contains(t, result, "Tokens: 50000")
249 assert.Contains(t, result, "Sentences: 2500")
250 assert.Contains(t, result, "Paragraphs: 500")
251 assert.Contains(t, result, "2. Test Corpus 2")
252 assert.Contains(t, result, "ID: corpus2")
253 assert.Contains(t, result, "Documents: 200")
254 assert.Contains(t, result, "Tokens: 75000")
255}
256
257func TestMetadataTool_formatCorpusStatistics(t *testing.T) {
258 client := &service.Client{}
259 tool := NewMetadataTool(client)
260
261 // Test minimal statistics
262 minimalStats := &service.StatisticsResponse{
263 Documents: 100,
264 Tokens: 50000,
265 }
266
267 result := tool.formatCorpusStatistics("test-corpus", minimalStats)
268 assert.Contains(t, result, "KorAP Corpus Statistics")
269 assert.Contains(t, result, "Corpus Query: test-corpus")
270 assert.Contains(t, result, "Documents: 100")
271 assert.Contains(t, result, "Tokens: 50000")
272
273 // Test complete statistics with additional fields
274 completeStats := &service.StatisticsResponse{
275 Documents: 200,
276 Tokens: 100000,
277 Sentences: 5000,
278 Paragraphs: 1000,
Akron708f3912025-06-17 12:26:02 +0200279 Fields: map[string]any{
Akronbd154ea2025-06-12 17:01:58 +0200280 "genre": "literature",
281 "language": "German",
282 "year": 2023,
283 },
284 }
285
286 result = tool.formatCorpusStatistics("complete-corpus", completeStats)
287 assert.Contains(t, result, "KorAP Corpus Statistics")
288 assert.Contains(t, result, "Corpus Query: complete-corpus")
289 assert.Contains(t, result, "Documents: 200")
290 assert.Contains(t, result, "Tokens: 100000")
291 assert.Contains(t, result, "Sentences: 5000")
292 assert.Contains(t, result, "Paragraphs: 1000")
293 assert.Contains(t, result, "Additional Fields:")
294 assert.Contains(t, result, "genre: literature")
295 assert.Contains(t, result, "language: German")
296 assert.Contains(t, result, "year: 2023")
297
298 // Test empty corpus query (all available data)
299 result = tool.formatCorpusStatistics("", minimalStats)
300 assert.Contains(t, result, "KorAP Corpus Statistics")
301 assert.Contains(t, result, "Corpus Query: (all available data)")
302 assert.Contains(t, result, "Documents: 100")
303 assert.Contains(t, result, "Tokens: 50000")
304}
Akron8db31c32025-06-17 12:22:41 +0200305
306func TestMetadataTool_SchemaCompliance(t *testing.T) {
307 // Test various parameter combinations against the schema
308 client := &service.Client{}
309 tool := NewMetadataTool(client)
310
311 tests := []struct {
312 name string
Akron708f3912025-06-17 12:26:02 +0200313 arguments map[string]any
Akron8db31c32025-06-17 12:22:41 +0200314 expectValid bool
315 errorMsg string
316 }{
317 {
318 name: "valid_list_minimal",
Akron708f3912025-06-17 12:26:02 +0200319 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200320 "action": "list",
321 },
322 expectValid: true,
323 },
324 {
325 name: "valid_statistics_minimal",
Akron708f3912025-06-17 12:26:02 +0200326 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200327 "action": "statistics",
328 },
329 expectValid: true,
330 },
331 {
332 name: "valid_statistics_with_corpus",
Akron708f3912025-06-17 12:26:02 +0200333 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200334 "action": "statistics",
335 "corpus": "test-corpus",
336 },
337 expectValid: true,
338 },
339 {
340 name: "valid_list_with_corpus_ignored",
Akron708f3912025-06-17 12:26:02 +0200341 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200342 "action": "list",
343 "corpus": "test-corpus", // Should be ignored for list action
344 },
345 expectValid: true,
346 },
347 {
348 name: "missing_required_action",
Akron708f3912025-06-17 12:26:02 +0200349 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200350 "corpus": "test-corpus",
351 },
352 expectValid: false,
353 errorMsg: "action parameter is required",
354 },
355 {
356 name: "invalid_action",
Akron708f3912025-06-17 12:26:02 +0200357 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200358 "action": "invalid",
359 },
360 expectValid: false,
361 errorMsg: "invalid action",
362 },
363 {
364 name: "empty_action",
Akron708f3912025-06-17 12:26:02 +0200365 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200366 "action": "",
367 },
368 expectValid: false,
369 errorMsg: "action is required and cannot be empty",
370 },
371 {
372 name: "invalid_corpus_format",
Akron708f3912025-06-17 12:26:02 +0200373 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200374 "action": "statistics",
375 "corpus": "invalid@corpus#format",
376 },
377 expectValid: false,
378 errorMsg: "collection query contains invalid characters",
379 },
380 {
381 name: "valid_corpus_with_boolean",
Akron708f3912025-06-17 12:26:02 +0200382 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200383 "action": "statistics",
384 "corpus": "corpus1 & corpus2",
385 },
386 expectValid: true,
387 },
388 {
389 name: "valid_collection_query",
Akron708f3912025-06-17 12:26:02 +0200390 arguments: map[string]any{
Akron8db31c32025-06-17 12:22:41 +0200391 "action": "statistics",
392 "corpus": "textClass = \"politics\" & pubDate in 2020",
393 },
394 expectValid: true,
395 },
396 }
397
398 for _, tt := range tests {
399 t.Run(tt.name, func(t *testing.T) {
400 request := mcp.CallToolRequest{
401 Params: mcp.CallToolParams{
402 Arguments: tt.arguments,
403 },
404 }
405
406 _, err := tool.Execute(context.Background(), request)
407
408 if tt.expectValid {
409 // For valid requests, we expect authentication/client errors, not validation errors
410 if err != nil {
411 assert.NotContains(t, err.Error(), "validation")
412 // Should fail at authentication or client configuration
413 assert.True(t,
414 strings.Contains(err.Error(), "authentication") ||
415 strings.Contains(err.Error(), "KorAP client not configured"),
416 "Expected authentication or client error, got: %s", err.Error())
417 }
418 } else {
419 // For invalid requests, we expect validation errors
420 assert.Error(t, err)
421 if tt.errorMsg != "" {
422 assert.Contains(t, err.Error(), tt.errorMsg)
423 }
424 }
425 })
426 }
427}