| package tools |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "reflect" |
| "testing" |
| |
| "github.com/korap/korap-mcp/service" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| ) |
| |
| // TestAllToolsSchemaCompliance ensures all tools have properly structured schemas |
| func TestAllToolsSchemaCompliance(t *testing.T) { |
| client := &service.Client{} |
| |
| tools := []Tool{ |
| NewSearchTool(client), |
| NewMetadataTool(client), |
| } |
| |
| for _, tool := range tools { |
| t.Run(tool.Name(), func(t *testing.T) { |
| schema := tool.InputSchema() |
| |
| // Verify basic schema structure |
| validateBasicSchemaStructure(t, schema, tool.Name()) |
| |
| // Verify schema is valid JSON |
| validateSchemaIsValidJSON(t, schema, tool.Name()) |
| |
| // Verify schema documentation completeness |
| validateSchemaDocumentation(t, schema, tool.Name()) |
| }) |
| } |
| } |
| |
| // validateBasicSchemaStructure checks that the schema has the required top-level properties |
| func validateBasicSchemaStructure(t *testing.T, schema map[string]any, toolName string) { |
| // Must be object type |
| assert.Equal(t, "object", schema["type"], "Tool %s schema must be object type", toolName) |
| |
| // Must have properties |
| properties, exists := schema["properties"] |
| assert.True(t, exists, "Tool %s schema must have properties", toolName) |
| assert.IsType(t, map[string]any{}, properties, "Tool %s properties must be an object", toolName) |
| |
| // Must have required array |
| required, exists := schema["required"] |
| assert.True(t, exists, "Tool %s schema must have required array", toolName) |
| assert.IsType(t, []string{}, required, "Tool %s required must be string array", toolName) |
| |
| // Should have title and description |
| assert.Contains(t, schema, "title", "Tool %s schema should have title", toolName) |
| assert.Contains(t, schema, "description", "Tool %s schema should have description", toolName) |
| |
| // Should prevent additional properties for strict validation |
| assert.Contains(t, schema, "additionalProperties", "Tool %s schema should specify additionalProperties", toolName) |
| assert.Equal(t, false, schema["additionalProperties"], "Tool %s should not allow additional properties", toolName) |
| } |
| |
| // validateSchemaIsValidJSON ensures the schema can be properly serialized to JSON |
| func validateSchemaIsValidJSON(t *testing.T, schema map[string]any, toolName string) { |
| jsonBytes, err := json.Marshal(schema) |
| require.NoError(t, err, "Tool %s schema must be serializable to JSON", toolName) |
| |
| var unmarshalled map[string]any |
| err = json.Unmarshal(jsonBytes, &unmarshalled) |
| require.NoError(t, err, "Tool %s schema JSON must be valid", toolName) |
| |
| // Verify the unmarshalled schema has the same structure |
| assert.Equal(t, schema["type"], unmarshalled["type"], "Tool %s schema type must survive JSON round-trip", toolName) |
| } |
| |
| // validateSchemaDocumentation checks that all properties have proper documentation |
| func validateSchemaDocumentation(t *testing.T, schema map[string]any, toolName string) { |
| properties, ok := schema["properties"].(map[string]any) |
| require.True(t, ok, "Tool %s properties must be accessible", toolName) |
| |
| for propName, propSchema := range properties { |
| propMap, ok := propSchema.(map[string]any) |
| require.True(t, ok, "Tool %s property %s must be an object", toolName, propName) |
| |
| // Must have type |
| assert.Contains(t, propMap, "type", "Tool %s property %s must have type", toolName, propName) |
| |
| // Must have description |
| assert.Contains(t, propMap, "description", "Tool %s property %s must have description", toolName, propName) |
| |
| description, ok := propMap["description"].(string) |
| assert.True(t, ok, "Tool %s property %s description must be string", toolName, propName) |
| assert.NotEmpty(t, description, "Tool %s property %s description must not be empty", toolName, propName) |
| assert.Greater(t, len(description), 10, "Tool %s property %s description should be descriptive", toolName, propName) |
| |
| // String properties should have examples |
| if propType, ok := propMap["type"].(string); ok && propType == "string" { |
| if enumValues, hasEnum := propMap["enum"]; hasEnum { |
| // Enum properties should have examples matching enum values |
| assert.Contains(t, propMap, "examples", "Tool %s property %s with enum should have examples", toolName, propName) |
| examples, ok := propMap["examples"].([]string) |
| assert.True(t, ok, "Tool %s property %s examples must be string array", toolName, propName) |
| |
| enumArray, ok := enumValues.([]string) |
| assert.True(t, ok, "Tool %s property %s enum must be string array", toolName, propName) |
| |
| // All examples should be valid enum values |
| for _, example := range examples { |
| assert.Contains(t, enumArray, example, "Tool %s property %s example '%s' must be valid enum value", toolName, propName, example) |
| } |
| } else { |
| // Regular string properties should have examples |
| assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName) |
| } |
| } |
| |
| // Integer properties should have examples and constraints |
| if propType, ok := propMap["type"].(string); ok && propType == "integer" { |
| assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName) |
| |
| // Should have minimum/maximum constraints |
| assert.Contains(t, propMap, "minimum", "Tool %s property %s should have minimum", toolName, propName) |
| assert.Contains(t, propMap, "maximum", "Tool %s property %s should have maximum", toolName, propName) |
| } |
| } |
| } |
| |
| // TestSchemaExamples verifies that schema examples are realistic and valid |
| func TestSchemaExamples(t *testing.T) { |
| client := &service.Client{} |
| |
| t.Run("SearchTool", func(t *testing.T) { |
| tool := NewSearchTool(client) |
| schema := tool.InputSchema() |
| |
| properties := schema["properties"].(map[string]any) |
| |
| // Test query examples |
| queryProp := properties["query"].(map[string]any) |
| examples := queryProp["examples"].([]string) |
| |
| for _, example := range examples { |
| assert.NotEmpty(t, example, "Query example should not be empty") |
| assert.LessOrEqual(t, len(example), 100, "Query example should be reasonably short") |
| } |
| |
| // Test corpus examples |
| corpusProp := properties["corpus"].(map[string]any) |
| corpusExamples := corpusProp["examples"].([]string) |
| |
| for _, example := range corpusExamples { |
| assert.NotEmpty(t, example, "Corpus example should not be empty") |
| // Should match the pattern defined in the schema |
| pattern := corpusProp["pattern"].(string) |
| assert.NotEmpty(t, pattern, "Corpus should have validation pattern") |
| } |
| }) |
| |
| t.Run("MetadataTool", func(t *testing.T) { |
| tool := NewMetadataTool(client) |
| schema := tool.InputSchema() |
| |
| properties := schema["properties"].(map[string]any) |
| |
| // Test action examples |
| actionProp := properties["action"].(map[string]any) |
| examples := actionProp["examples"].([]string) |
| enumValues := actionProp["enum"].([]string) |
| |
| for _, example := range examples { |
| assert.Contains(t, enumValues, example, "Action example should be valid enum value") |
| } |
| }) |
| } |
| |
| // TestSchemaConstraints verifies that schema constraints are reasonable |
| func TestSchemaConstraints(t *testing.T) { |
| client := &service.Client{} |
| |
| t.Run("SearchTool", func(t *testing.T) { |
| tool := NewSearchTool(client) |
| schema := tool.InputSchema() |
| properties := schema["properties"].(map[string]any) |
| |
| // Test query constraints |
| queryProp := properties["query"].(map[string]any) |
| minLength := queryProp["minLength"].(int) |
| maxLength := queryProp["maxLength"].(int) |
| |
| assert.Greater(t, minLength, 0, "Query minimum length should be positive") |
| assert.Greater(t, maxLength, minLength, "Query maximum length should be greater than minimum") |
| assert.LessOrEqual(t, maxLength, 10000, "Query maximum length should be reasonable") |
| |
| // Test count constraints |
| countProp := properties["count"].(map[string]any) |
| minimum := countProp["minimum"].(int) |
| maximum := countProp["maximum"].(int) |
| defaultValue := countProp["default"].(int) |
| |
| assert.GreaterOrEqual(t, minimum, 0, "Count minimum should be non-negative") |
| assert.Greater(t, maximum, minimum, "Count maximum should be greater than minimum") |
| assert.GreaterOrEqual(t, defaultValue, minimum, "Count default should be >= minimum") |
| assert.LessOrEqual(t, defaultValue, maximum, "Count default should be <= maximum") |
| }) |
| } |
| |
| // TestSchemaVersioning ensures schema structure is backwards compatible |
| func TestSchemaVersioning(t *testing.T) { |
| client := &service.Client{} |
| |
| // This test would be extended when we add new versions |
| // For now, we ensure the basic structure is stable |
| |
| tools := []Tool{ |
| NewSearchTool(client), |
| NewMetadataTool(client), |
| } |
| |
| for _, tool := range tools { |
| t.Run(tool.Name(), func(t *testing.T) { |
| schema := tool.InputSchema() |
| |
| // Core schema structure must remain stable |
| assert.Equal(t, "object", schema["type"]) |
| assert.Contains(t, schema, "properties") |
| assert.Contains(t, schema, "required") |
| |
| // Tool-specific stability checks |
| switch tool.Name() { |
| case "korap_search": |
| properties := schema["properties"].(map[string]any) |
| assert.Contains(t, properties, "query", "Search tool must always have query parameter") |
| |
| required := schema["required"].([]string) |
| assert.Contains(t, required, "query", "Search tool must always require query parameter") |
| |
| case "korap_metadata": |
| properties := schema["properties"].(map[string]any) |
| assert.Contains(t, properties, "action", "Metadata tool must always have action parameter") |
| |
| required := schema["required"].([]string) |
| assert.Contains(t, required, "action", "Metadata tool must always require action parameter") |
| } |
| }) |
| } |
| } |
| |
| // BenchmarkSchemaGeneration measures schema generation performance |
| func BenchmarkSchemaGeneration(b *testing.B) { |
| client := &service.Client{} |
| |
| tools := []Tool{ |
| NewSearchTool(client), |
| NewMetadataTool(client), |
| } |
| |
| for _, tool := range tools { |
| b.Run(tool.Name(), func(b *testing.B) { |
| for i := 0; i < b.N; i++ { |
| schema := tool.InputSchema() |
| _ = schema // Prevent optimization |
| } |
| }) |
| } |
| } |
| |
| // TestSchemaDeepCopy ensures schemas are independent instances |
| func TestSchemaDeepCopy(t *testing.T) { |
| client := &service.Client{} |
| tool := NewSearchTool(client) |
| |
| // Get two schema instances |
| schema1 := tool.InputSchema() |
| schema2 := tool.InputSchema() |
| |
| // They should be equal in content but different instances |
| assert.True(t, reflect.DeepEqual(schema1, schema2), "Schemas should have equal content") |
| |
| // Modifying one should not affect the other |
| schema1["modified"] = true |
| assert.NotContains(t, schema2, "modified", "Schema instances should be independent") |
| } |
| |
| // TestSchemaHelpGeneration verifies schema can be used to generate help text |
| func TestSchemaHelpGeneration(t *testing.T) { |
| client := &service.Client{} |
| |
| tools := []Tool{ |
| NewSearchTool(client), |
| NewMetadataTool(client), |
| } |
| |
| for _, tool := range tools { |
| t.Run(tool.Name(), func(t *testing.T) { |
| schema := tool.InputSchema() |
| |
| // Generate help text from schema |
| helpText := generateHelpText(schema, tool.Name()) |
| |
| // Verify help text contains essential information |
| assert.Contains(t, helpText, tool.Name(), "Help text should contain tool name") |
| assert.Contains(t, helpText, "Parameters:", "Help text should list parameters") |
| |
| // Should contain information about required parameters |
| required := schema["required"].([]string) |
| for _, param := range required { |
| assert.Contains(t, helpText, param, "Help text should mention required parameter %s", param) |
| assert.Contains(t, helpText, "(required)", "Help text should mark required parameters") |
| } |
| }) |
| } |
| } |
| |
| // generateHelpText creates human-readable help text from a schema |
| func generateHelpText(schema map[string]any, toolName string) string { |
| help := fmt.Sprintf("Tool: %s\n", toolName) |
| |
| if desc, ok := schema["description"].(string); ok { |
| help += fmt.Sprintf("Description: %s\n\n", desc) |
| } |
| |
| properties, ok := schema["properties"].(map[string]any) |
| if !ok { |
| return help |
| } |
| |
| required, _ := schema["required"].([]string) |
| requiredMap := make(map[string]bool) |
| for _, req := range required { |
| requiredMap[req] = true |
| } |
| |
| help += "Parameters:\n" |
| for paramName, paramSchema := range properties { |
| paramMap, ok := paramSchema.(map[string]any) |
| if !ok { |
| continue |
| } |
| |
| requiredText := "" |
| if requiredMap[paramName] { |
| requiredText = " (required)" |
| } |
| |
| paramType, _ := paramMap["type"].(string) |
| description, _ := paramMap["description"].(string) |
| |
| help += fmt.Sprintf(" %s (%s)%s: %s\n", paramName, paramType, requiredText, description) |
| |
| // Add enum values if present |
| if enum, ok := paramMap["enum"].([]string); ok { |
| help += fmt.Sprintf(" Allowed values: %v\n", enum) |
| } |
| |
| // Add examples if present |
| if examples, ok := paramMap["examples"]; ok { |
| help += fmt.Sprintf(" Examples: %v\n", examples) |
| } |
| } |
| |
| return help |
| } |