mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
614 lines
12 KiB
Go
614 lines
12 KiB
Go
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 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))
|
|
}
|
|
}
|