blob: 240d0c1094bf10f199ea76f84d47e77b092143d0 [file] [log] [blame]
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
}