package util import ( "encoding/json" "reflect" "strings" "testing" ) func TestCleanJSONSchemaForGemini_ConstToEnum(t *testing.T) { input := `{ "type": "object", "properties": { "kind": { "type": "string", "const": "InsightVizNode" } } }` expected := `{ "type": "object", "properties": { "kind": { "type": "string", "enum": ["InsightVizNode"] } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_TypeFlattening_Nullable(t *testing.T) { input := `{ "type": "object", "properties": { "name": { "type": ["string", "null"] }, "other": { "type": "string" } }, "required": ["name", "other"] }` expected := `{ "type": "object", "properties": { "name": { "type": "string", "description": "(nullable)" }, "other": { "type": "string" } }, "required": ["other"] }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_ConstraintsToDescription(t *testing.T) { input := `{ "type": "object", "properties": { "tags": { "type": "array", "description": "List of tags", "minItems": 1 }, "name": { "type": "string", "description": "User name", "minLength": 3 } } }` result := CleanJSONSchemaForGemini(input) // minItems should be REMOVED and moved to description if strings.Contains(result, `"minItems"`) { t.Errorf("minItems keyword should be removed") } if !strings.Contains(result, "minItems: 1") { t.Errorf("minItems hint missing in description") } // minLength should be moved to description if !strings.Contains(result, "minLength: 3") { t.Errorf("minLength hint missing in description") } if strings.Contains(result, `"minLength":`) || strings.Contains(result, `"minLength" :`) { t.Errorf("minLength keyword should be removed") } } func TestCleanJSONSchemaForGemini_AnyOfFlattening_SmartSelection(t *testing.T) { input := `{ "type": "object", "properties": { "query": { "anyOf": [ { "type": "null" }, { "type": "object", "properties": { "kind": { "type": "string" } } } ] } } }` expected := `{ "type": "object", "properties": { "query": { "type": "object", "description": "Accepts: null | object", "properties": { "kind": { "type": "string" } } } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_OneOfFlattening(t *testing.T) { input := `{ "type": "object", "properties": { "config": { "oneOf": [ { "type": "string" }, { "type": "integer" } ] } } }` expected := `{ "type": "object", "properties": { "config": { "type": "string", "description": "Accepts: string | integer" } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_AllOfMerging(t *testing.T) { input := `{ "type": "object", "allOf": [ { "properties": { "a": { "type": "string" } }, "required": ["a"] }, { "properties": { "b": { "type": "integer" } }, "required": ["b"] } ] }` expected := `{ "type": "object", "properties": { "a": { "type": "string" }, "b": { "type": "integer" } }, "required": ["a", "b"] }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_RefHandling(t *testing.T) { input := `{ "definitions": { "User": { "type": "object", "properties": { "name": { "type": "string" } } } }, "type": "object", "properties": { "customer": { "$ref": "#/definitions/User" } } }` expected := `{ "type": "object", "properties": { "customer": { "type": "object", "description": "See: User" } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_RefHandling_DescriptionEscaping(t *testing.T) { input := `{ "definitions": { "User": { "type": "object", "properties": { "name": { "type": "string" } } } }, "type": "object", "properties": { "customer": { "description": "He said \"hi\"\\nsecond line", "$ref": "#/definitions/User" } } }` expected := `{ "type": "object", "properties": { "customer": { "type": "object", "description": "He said \"hi\"\\nsecond line (See: User)" } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_CyclicRefDefaults(t *testing.T) { input := `{ "definitions": { "Node": { "type": "object", "properties": { "child": { "$ref": "#/definitions/Node" } } } }, "$ref": "#/definitions/Node" }` result := CleanJSONSchemaForGemini(input) var resMap map[string]interface{} json.Unmarshal([]byte(result), &resMap) if resMap["type"] != "object" { t.Errorf("Expected type: object, got: %v", resMap["type"]) } desc, ok := resMap["description"].(string) if !ok || !strings.Contains(desc, "Node") { t.Errorf("Expected description hint containing 'Node', got: %v", resMap["description"]) } } func TestCleanJSONSchemaForGemini_RequiredCleanup(t *testing.T) { input := `{ "type": "object", "properties": { "a": {"type": "string"}, "b": {"type": "string"} }, "required": ["a", "b", "c"] }` expected := `{ "type": "object", "properties": { "a": {"type": "string"}, "b": {"type": "string"} }, "required": ["a", "b"] }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_AllOfMerging_DotKeys(t *testing.T) { input := `{ "type": "object", "allOf": [ { "properties": { "my.param": { "type": "string" } }, "required": ["my.param"] }, { "properties": { "b": { "type": "integer" } }, "required": ["b"] } ] }` expected := `{ "type": "object", "properties": { "my.param": { "type": "string" }, "b": { "type": "integer" } }, "required": ["my.param", "b"] }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_PropertyNameCollision(t *testing.T) { // A tool has an argument named "pattern" - should NOT be treated as a constraint input := `{ "type": "object", "properties": { "pattern": { "type": "string", "description": "The regex pattern" } }, "required": ["pattern"] }` expected := `{ "type": "object", "properties": { "pattern": { "type": "string", "description": "The regex pattern" } }, "required": ["pattern"] }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) var resMap map[string]interface{} json.Unmarshal([]byte(result), &resMap) props, _ := resMap["properties"].(map[string]interface{}) if _, ok := props["description"]; ok { t.Errorf("Invalid 'description' property injected into properties map") } } func TestCleanJSONSchemaForGemini_DotKeys(t *testing.T) { input := `{ "type": "object", "properties": { "my.param": { "type": "string", "$ref": "#/definitions/MyType" } }, "definitions": { "MyType": { "type": "string" } } }` result := CleanJSONSchemaForGemini(input) var resMap map[string]interface{} if err := json.Unmarshal([]byte(result), &resMap); err != nil { t.Fatalf("Failed to unmarshal result: %v", err) } props, ok := resMap["properties"].(map[string]interface{}) if !ok { t.Fatalf("properties missing") } if val, ok := props["my.param"]; !ok { t.Fatalf("Key 'my.param' is missing. Result: %s", result) } else { valMap, _ := val.(map[string]interface{}) if _, hasRef := valMap["$ref"]; hasRef { t.Errorf("Key 'my.param' still contains $ref") } if _, ok := props["my"]; ok { t.Errorf("Artifact key 'my' created by sjson splitting") } } } func TestCleanJSONSchemaForGemini_AnyOfAlternativeHints(t *testing.T) { input := `{ "type": "object", "properties": { "value": { "anyOf": [ { "type": "string" }, { "type": "integer" }, { "type": "null" } ] } } }` result := CleanJSONSchemaForGemini(input) if !strings.Contains(result, "Accepts:") { t.Errorf("Expected alternative types hint, got: %s", result) } if !strings.Contains(result, "string") || !strings.Contains(result, "integer") { t.Errorf("Expected all alternative types in hint, got: %s", result) } } func TestCleanJSONSchemaForGemini_NullableHint(t *testing.T) { input := `{ "type": "object", "properties": { "name": { "type": ["string", "null"], "description": "User name" } }, "required": ["name"] }` result := CleanJSONSchemaForGemini(input) if !strings.Contains(result, "(nullable)") { t.Errorf("Expected nullable hint, got: %s", result) } if !strings.Contains(result, "User name") { t.Errorf("Expected original description to be preserved, got: %s", result) } } func TestCleanJSONSchemaForGemini_TypeFlattening_Nullable_DotKey(t *testing.T) { input := `{ "type": "object", "properties": { "my.param": { "type": ["string", "null"] }, "other": { "type": "string" } }, "required": ["my.param", "other"] }` expected := `{ "type": "object", "properties": { "my.param": { "type": "string", "description": "(nullable)" }, "other": { "type": "string" } }, "required": ["other"] }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_EnumHint(t *testing.T) { input := `{ "type": "object", "properties": { "status": { "type": "string", "enum": ["active", "inactive", "pending"], "description": "Current status" } } }` result := CleanJSONSchemaForGemini(input) if !strings.Contains(result, "Allowed:") { t.Errorf("Expected enum values hint, got: %s", result) } if !strings.Contains(result, "active") || !strings.Contains(result, "inactive") { t.Errorf("Expected enum values in hint, got: %s", result) } } func TestCleanJSONSchemaForGemini_AdditionalPropertiesHint(t *testing.T) { input := `{ "type": "object", "properties": { "name": { "type": "string" } }, "additionalProperties": false }` result := CleanJSONSchemaForGemini(input) if !strings.Contains(result, "No extra properties allowed") { t.Errorf("Expected additionalProperties hint, got: %s", result) } } func TestCleanJSONSchemaForGemini_AnyOfFlattening_PreservesDescription(t *testing.T) { input := `{ "type": "object", "properties": { "config": { "description": "Parent desc", "anyOf": [ { "type": "string", "description": "Child desc" }, { "type": "integer" } ] } } }` expected := `{ "type": "object", "properties": { "config": { "type": "string", "description": "Parent desc (Child desc) (Accepts: string | integer)" } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) } func TestCleanJSONSchemaForGemini_SingleEnumNoHint(t *testing.T) { input := `{ "type": "object", "properties": { "kind": { "type": "string", "enum": ["fixed"] } } }` result := CleanJSONSchemaForGemini(input) if strings.Contains(result, "Allowed:") { t.Errorf("Single value enum should not add Allowed hint, got: %s", result) } } func TestCleanJSONSchemaForGemini_MultipleNonNullTypes(t *testing.T) { input := `{ "type": "object", "properties": { "value": { "type": ["string", "integer", "boolean"] } } }` result := CleanJSONSchemaForGemini(input) if !strings.Contains(result, "Accepts:") { t.Errorf("Expected multiple types hint, got: %s", result) } if !strings.Contains(result, "string") || !strings.Contains(result, "integer") || !strings.Contains(result, "boolean") { t.Errorf("Expected all types in hint, got: %s", result) } } func TestCleanJSONSchemaForGemini_PropertyNamesRemoval(t *testing.T) { // propertyNames is used to validate object property names (e.g., must match a pattern) // Gemini doesn't support this keyword and will reject requests containing it input := `{ "type": "object", "properties": { "metadata": { "type": "object", "propertyNames": { "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" }, "additionalProperties": { "type": "string" } } } }` expected := `{ "type": "object", "properties": { "metadata": { "type": "object" } } }` result := CleanJSONSchemaForGemini(input) compareJSON(t, expected, result) // Verify propertyNames is completely removed if strings.Contains(result, "propertyNames") { t.Errorf("propertyNames keyword should be removed, got: %s", result) } } func TestCleanJSONSchemaForGemini_PropertyNamesRemoval_Nested(t *testing.T) { // Test deeply nested propertyNames (as seen in real Claude tool schemas) input := `{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "config": { "type": "object", "propertyNames": { "type": "string" } } } } } } }` result := CleanJSONSchemaForGemini(input) if strings.Contains(result, "propertyNames") { t.Errorf("Nested propertyNames should be removed, got: %s", result) } } func compareJSON(t *testing.T, expectedJSON, actualJSON string) { var expMap, actMap map[string]interface{} errExp := json.Unmarshal([]byte(expectedJSON), &expMap) errAct := json.Unmarshal([]byte(actualJSON), &actMap) if errExp != nil || errAct != nil { t.Fatalf("JSON Unmarshal error. Exp: %v, Act: %v", errExp, errAct) } if !reflect.DeepEqual(expMap, actMap) { expBytes, _ := json.MarshalIndent(expMap, "", " ") actBytes, _ := json.MarshalIndent(actMap, "", " ") t.Errorf("JSON mismatch:\nExpected:\n%s\n\nActual:\n%s", string(expBytes), string(actBytes)) } }