| package tools |
| |
| import ( |
| "context" |
| "testing" |
| |
| "github.com/korap/korap-mcp/service" |
| "github.com/mark3labs/mcp-go/mcp" |
| "github.com/stretchr/testify/assert" |
| ) |
| |
| func TestSearchTool_Name(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| assert.Equal(t, "korap_search", tool.Name()) |
| } |
| |
| func TestSearchTool_Description(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| expected := "Search for words or phrases in KorAP corpora using various query languages" |
| assert.Equal(t, expected, tool.Description()) |
| } |
| |
| func TestSearchTool_InputSchema(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| schema := tool.InputSchema() |
| |
| // Verify it's an object type |
| assert.Equal(t, "object", schema["type"]) |
| assert.Equal(t, "KorAP Search Parameters", schema["title"]) |
| assert.Contains(t, schema["description"], "Parameters for searching text corpora") |
| assert.Equal(t, false, schema["additionalProperties"]) |
| |
| // Verify properties exist |
| properties, ok := schema["properties"].(map[string]any) |
| assert.True(t, ok) |
| assert.Contains(t, properties, "query") |
| assert.Contains(t, properties, "query_language") |
| assert.Contains(t, properties, "corpus") |
| assert.Contains(t, properties, "count") |
| |
| // Verify query property details |
| query, ok := properties["query"].(map[string]any) |
| assert.True(t, ok) |
| assert.Equal(t, "string", query["type"]) |
| assert.Contains(t, query["description"], "search query") |
| assert.Equal(t, 1, query["minLength"]) |
| assert.Equal(t, 1000, query["maxLength"]) |
| examples, ok := query["examples"].([]string) |
| assert.True(t, ok) |
| assert.Contains(t, examples, "Haus") |
| |
| // Verify query_language property details |
| queryLang, ok := properties["query_language"].(map[string]any) |
| assert.True(t, ok) |
| assert.Equal(t, "string", queryLang["type"]) |
| assert.Contains(t, queryLang["description"], "Query language to use for parsing") |
| enum, ok := queryLang["enum"].([]string) |
| assert.True(t, ok) |
| assert.Contains(t, enum, "poliqarp") |
| assert.Contains(t, enum, "cosmas2") |
| assert.Contains(t, enum, "annis") |
| assert.Contains(t, enum, "cql") |
| assert.Contains(t, enum, "cqp") |
| assert.Contains(t, enum, "fcsql") |
| assert.Equal(t, "poliqarp", queryLang["default"]) |
| |
| // Verify corpus property details |
| corpus, ok := properties["corpus"].(map[string]any) |
| assert.True(t, ok) |
| assert.Equal(t, "string", corpus["type"]) |
| assert.Contains(t, corpus["description"], "Virtual corpus query") |
| assert.NotEmpty(t, corpus["pattern"]) |
| corpusExamples, ok := corpus["examples"].([]string) |
| assert.True(t, ok) |
| assert.Contains(t, corpusExamples, "corpusSigle = \"GOE\"") |
| |
| // Verify count property details |
| count, ok := properties["count"].(map[string]any) |
| assert.True(t, ok) |
| assert.Equal(t, "integer", count["type"]) |
| assert.Contains(t, count["description"], "Maximum number") |
| assert.Equal(t, 0, count["minimum"]) |
| assert.Equal(t, 10000, count["maximum"]) |
| assert.Equal(t, 25, count["default"]) |
| countExamples, ok := count["examples"].([]any) |
| assert.True(t, ok) |
| assert.Contains(t, countExamples, 25) |
| |
| // Verify required fields |
| required, ok := schema["required"].([]string) |
| assert.True(t, ok) |
| assert.Contains(t, required, "query") |
| assert.Len(t, required, 1) // Only query should be required |
| } |
| |
| func TestSearchTool_SchemaCompliance(t *testing.T) { |
| // Test various parameter combinations against the schema |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| tests := []struct { |
| name string |
| arguments map[string]any |
| expectValid bool |
| errorMsg string |
| }{ |
| { |
| name: "valid_minimal", |
| arguments: map[string]any{ |
| "query": "test", |
| }, |
| expectValid: true, |
| }, |
| { |
| name: "valid_full", |
| arguments: map[string]any{ |
| "query": "word", |
| "query_language": "cosmas2", |
| "corpus": "test-corpus", |
| "count": 10, |
| }, |
| expectValid: true, |
| }, |
| { |
| name: "missing_required_query", |
| arguments: map[string]any{ |
| "query_language": "poliqarp", |
| }, |
| expectValid: false, |
| errorMsg: "query parameter is required", |
| }, |
| { |
| name: "invalid_query_language", |
| arguments: map[string]any{ |
| "query": "test", |
| "query_language": "invalid", |
| }, |
| expectValid: false, |
| errorMsg: "invalid query language", |
| }, |
| { |
| name: "invalid_count_negative", |
| arguments: map[string]any{ |
| "query": "test", |
| "count": -1, |
| }, |
| expectValid: false, |
| errorMsg: "count must be", |
| }, |
| { |
| name: "invalid_count_too_large", |
| arguments: map[string]any{ |
| "query": "test", |
| "count": 20000, |
| }, |
| expectValid: false, |
| errorMsg: "count must be", |
| }, |
| { |
| name: "empty_query", |
| arguments: map[string]any{ |
| "query": "", |
| }, |
| expectValid: false, |
| errorMsg: "query is required and cannot be empty", |
| }, |
| { |
| name: "count_zero_valid", |
| arguments: map[string]any{ |
| "query": "test", |
| "count": 0, |
| }, |
| expectValid: true, // Zero count should be valid (uses default behavior) |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| request := mcp.CallToolRequest{ |
| Params: mcp.CallToolParams{ |
| Arguments: tt.arguments, |
| }, |
| } |
| |
| _, err := tool.Execute(context.Background(), request) |
| |
| if tt.expectValid { |
| // For valid requests, we expect authentication/client errors, not validation errors |
| if err != nil { |
| assert.NotContains(t, err.Error(), "validation") |
| assert.Contains(t, err.Error(), "authentication") |
| } |
| } else { |
| // For invalid requests, we expect validation errors |
| assert.Error(t, err) |
| if tt.errorMsg != "" { |
| assert.Contains(t, err.Error(), tt.errorMsg) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestNewSearchTool(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| assert.NotNil(t, tool) |
| assert.Equal(t, client, tool.client) |
| } |
| |
| func TestSearchTool_Execute_MissingQuery(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| // Create request without query parameter |
| request := mcp.CallToolRequest{ |
| Params: mcp.CallToolParams{ |
| Arguments: map[string]any{}, |
| }, |
| } |
| |
| _, err := tool.Execute(context.Background(), request) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "query parameter is required") |
| } |
| |
| func TestSearchTool_Execute_NilClient(t *testing.T) { |
| tool := NewSearchTool(nil) |
| |
| request := mcp.CallToolRequest{ |
| Params: mcp.CallToolParams{ |
| Arguments: map[string]any{ |
| "query": "test", |
| }, |
| }, |
| } |
| |
| _, err := tool.Execute(context.Background(), request) |
| assert.Error(t, err) |
| assert.Contains(t, err.Error(), "KorAP client not configured") |
| } |
| |
| func TestSearchTool_Execute_ParameterExtraction(t *testing.T) { |
| // This test verifies that parameters are extracted correctly |
| // It should fail with authentication error since we don't have a mock server |
| // but we can verify the parameters were parsed correctly by checking the log messages |
| |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| tests := []struct { |
| name string |
| arguments map[string]any |
| expectErr bool |
| }{ |
| { |
| name: "minimal_query", |
| arguments: map[string]any{ |
| "query": "test", |
| }, |
| expectErr: true, // Will fail at authentication |
| }, |
| { |
| name: "full_parameters", |
| arguments: map[string]any{ |
| "query": "word", |
| "query_language": "cosmas2", |
| "corpus": "test-corpus", |
| "count": 10, |
| }, |
| expectErr: true, // Will fail at authentication |
| }, |
| { |
| name: "invalid_count_type", |
| arguments: map[string]any{ |
| "query": "test", |
| "count": "invalid", // Should use default |
| }, |
| expectErr: true, // Will fail at authentication |
| }, |
| } |
| |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| request := mcp.CallToolRequest{ |
| Params: mcp.CallToolParams{ |
| Arguments: tt.arguments, |
| }, |
| } |
| |
| _, err := tool.Execute(context.Background(), request) |
| if tt.expectErr { |
| assert.Error(t, err) |
| } |
| }) |
| } |
| } |
| |
| func TestSearchTool_formatSearchResults(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| // Test empty response |
| emptyResponse := &service.SearchResponse{ |
| Query: service.SearchQuery{ |
| Query: "test", |
| QueryLang: "poliqarp", |
| }, |
| Meta: service.SearchMeta{ |
| TotalResults: 0, |
| Count: 0, |
| StartIndex: 0, |
| }, |
| Matches: []service.SearchMatch{}, |
| } |
| |
| result := tool.formatSearchResults(emptyResponse) |
| assert.Contains(t, result, "KorAP Search Results") |
| assert.Contains(t, result, "Query: test") |
| assert.Contains(t, result, "Query Language: poliqarp") |
| assert.Contains(t, result, "Total Results: 0") |
| assert.Contains(t, result, "No matches found") |
| |
| // Test response with matches |
| responseWithMatches := &service.SearchResponse{ |
| Query: service.SearchQuery{ |
| Query: "word", |
| QueryLang: "poliqarp", |
| Collection: "test-corpus", |
| }, |
| Meta: service.SearchMeta{ |
| TotalResults: 5, |
| Count: 2, |
| StartIndex: 0, |
| SearchTime: 0.123, |
| }, |
| Matches: []service.SearchMatch{ |
| { |
| TextSigle: "text1", |
| Snippet: "This is a test snippet", |
| PubPlace: "Berlin", |
| MatchID: "match1", |
| Position: 10, |
| }, |
| { |
| TextSigle: "text2", |
| Snippet: "Another test snippet", |
| Position: 25, |
| }, |
| }, |
| } |
| |
| result = tool.formatSearchResults(responseWithMatches) |
| assert.Contains(t, result, "Query: word") |
| assert.Contains(t, result, "Query Language: poliqarp") |
| assert.Contains(t, result, "Corpus: test-corpus") |
| assert.Contains(t, result, "Total Results: 5") |
| assert.Contains(t, result, "Shown: 1-2") |
| assert.Contains(t, result, "Search Time: 0.123 seconds") |
| assert.Contains(t, result, "1. Text: text1") |
| assert.Contains(t, result, "Snippet: This is a test snippet") |
| assert.Contains(t, result, "Publication: Berlin") |
| assert.Contains(t, result, "2. Text: text2") |
| assert.Contains(t, result, "Position: 25") |
| |
| // Test response with warnings |
| responseWithWarnings := &service.SearchResponse{ |
| Query: service.SearchQuery{ |
| Query: "test", |
| CutOff: true, |
| TimeExceeded: true, |
| }, |
| Meta: service.SearchMeta{}, |
| Matches: []service.SearchMatch{}, |
| } |
| |
| result = tool.formatSearchResults(responseWithWarnings) |
| assert.Contains(t, result, "Results were cut off due to limits") |
| assert.Contains(t, result, "Search time limit was exceeded") |
| } |