package wire import ( "encoding/json" "errors" "fmt" "strings" "testing" ) // TestDereferenceSchema_NoRef 验证不含 $ref 的 schema 原样返回(零开销快速路径). func TestDereferenceSchema_NoRef(t *testing.T) { input := json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`) out, err := DereferenceSchema(input) if err != nil { t.Fatalf("unexpected error: %v", err) } // 结果应等价于输入(字段顺序可能不同,比较解析后的值) var inObj, outObj any _ = json.Unmarshal(input, &inObj) _ = json.Unmarshal(out, &outObj) inJ, _ := json.Marshal(inObj) outJ, _ := json.Marshal(outObj) if string(inJ) != string(outJ) { t.Errorf("no-ref schema changed:\n in: %s\n out: %s", input, out) } } // TestDereferenceSchema_SingleRef 验证单层 $ref 展开: // properties.addr.$ref → $defs.Address 内联. func TestDereferenceSchema_SingleRef(t *testing.T) { input := json.RawMessage(`{ "type": "object", "$defs": { "Address": { "type": "object", "properties": { "city": {"type": "string"}, "zip": {"type": "string"} } } }, "properties": { "name": {"type": "string"}, "addr": {"$ref": "#/$defs/Address"} } }`) out, err := DereferenceSchema(input) if err != nil { t.Fatalf("unexpected error: %v", err) } // 验证:顶层不含 $defs var outObj map[string]json.RawMessage if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal output: %v", err) } if _, has := outObj["$defs"]; has { t.Error("$defs should be removed after deref") } // 验证:addr 已展开为 Address 对象(含 type=object) propsRaw := outObj["properties"] var props map[string]json.RawMessage if err := json.Unmarshal(propsRaw, &props); err != nil { t.Fatalf("unmarshal properties: %v", err) } addrRaw, ok := props["addr"] if !ok { t.Fatal("addr property missing") } var addr map[string]json.RawMessage if err := json.Unmarshal(addrRaw, &addr); err != nil { t.Fatalf("unmarshal addr: %v", err) } if _, hasRef := addr["$ref"]; hasRef { t.Error("$ref should be replaced in addr") } var typ string if err := json.Unmarshal(addr["type"], &typ); err != nil || typ != "object" { t.Errorf("addr.type should be 'object', got %s", addr["type"]) } } // TestDereferenceSchema_NestedRef 验证嵌套 $ref:A 引用 B,B 引用 C,全部展开. func TestDereferenceSchema_NestedRef(t *testing.T) { input := json.RawMessage(`{ "type": "object", "$defs": { "C": {"type": "string"}, "B": { "type": "object", "properties": { "c": {"$ref": "#/$defs/C"} } }, "A": { "type": "object", "properties": { "b": {"$ref": "#/$defs/B"} } } }, "properties": { "a": {"$ref": "#/$defs/A"} } }`) out, err := DereferenceSchema(input) if err != nil { t.Fatalf("unexpected error: %v", err) } // 验证输出中不含任何 $ref if contains(out, `"$ref"`) { t.Errorf("output still contains $ref: %s", out) } // 验证 a.properties.b.properties.c.type = "string"(三层展开) var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) a := props["a"].(map[string]any) aProps := a["properties"].(map[string]any) b := aProps["b"].(map[string]any) bProps := b["properties"].(map[string]any) c := bProps["c"].(map[string]any) if c["type"] != "string" { t.Errorf("expected c.type=string, got %v", c["type"]) } } // TestDereferenceSchema_CircularRef 验证循环引用检测返回 ErrCircularRef. func TestDereferenceSchema_CircularRef(t *testing.T) { // A 引用 B,B 引用 A--经典循环 input := json.RawMessage(`{ "type": "object", "$defs": { "A": { "type": "object", "properties": { "b": {"$ref": "#/$defs/B"} } }, "B": { "type": "object", "properties": { "a": {"$ref": "#/$defs/A"} } } }, "properties": { "root": {"$ref": "#/$defs/A"} } }`) _, err := DereferenceSchema(input) if !errors.Is(err, ErrCircularRef) { t.Errorf("expected ErrCircularRef, got: %v", err) } } // TestDereferenceSchema_ExternalRef 验证外部 $ref 返回 ErrExternalRef. func TestDereferenceSchema_ExternalRef(t *testing.T) { input := json.RawMessage(`{ "type": "object", "properties": { "addr": {"$ref": "https://example.com/schema/address.json"} } }`) _, err := DereferenceSchema(input) if !errors.Is(err, ErrExternalRef) { t.Errorf("expected ErrExternalRef, got: %v", err) } } // TestDereferenceSchema_MultipleDefs 验证多个 $defs 定义均可正确展开. func TestDereferenceSchema_MultipleDefs(t *testing.T) { input := json.RawMessage(`{ "type": "object", "$defs": { "Name": {"type": "string", "maxLength": 100}, "Age": {"type": "integer", "minimum": 0}, "Email": {"type": "string", "format": "email"} }, "properties": { "name": {"$ref": "#/$defs/Name"}, "age": {"$ref": "#/$defs/Age"}, "email": {"$ref": "#/$defs/Email"} } }`) out, err := DereferenceSchema(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if contains(out, `"$ref"`) { t.Errorf("output still contains $ref: %s", out) } if contains(out, `"$defs"`) { t.Errorf("output still contains $defs: %s", out) } var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) name := props["name"].(map[string]any) if name["type"] != "string" { t.Errorf("name.type: want string, got %v", name["type"]) } age := props["age"].(map[string]any) if age["type"] != "integer" { t.Errorf("age.type: want integer, got %v", age["type"]) } email := props["email"].(map[string]any) if email["format"] != "email" { t.Errorf("email.format: want email, got %v", email["format"]) } } // TestDereferenceSchema_Empty 验证空输入原样返回. func TestDereferenceSchema_Empty(t *testing.T) { out, err := DereferenceSchema(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(out) != 0 { t.Errorf("expected empty, got %s", out) } } // TestDereferenceSchema_RefInArray 验证数组元素中的 $ref 也被展开. func TestDereferenceSchema_RefInArray(t *testing.T) { input := json.RawMessage(`{ "type": "object", "$defs": { "Tag": {"type": "string"} }, "properties": { "tags": { "type": "array", "items": {"$ref": "#/$defs/Tag"} } } }`) out, err := DereferenceSchema(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if contains(out, `"$ref"`) { t.Errorf("output still contains $ref: %s", out) } var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) tags := props["tags"].(map[string]any) items := tags["items"].(map[string]any) if items["type"] != "string" { t.Errorf("tags.items.type: want string, got %v", items["type"]) } } // ───────────────────────────────────────────────────────────────────────────── // CAP-6: AdaptSchema 测试 // ───────────────────────────────────────────────────────────────────────────── // TestAdaptSchema_AnthropicRemovesNumericConstraints 验证 Anthropic 路径下 // minimum/maximum/multipleOf 等数值约束被移除,并写入 description. func TestAdaptSchema_AnthropicRemovesNumericConstraints(t *testing.T) { input := json.RawMessage(`{ "type": "object", "properties": { "count": { "type": "integer", "minimum": 1, "maximum": 100, "description": "number of items" }, "ratio": { "type": "number", "multipleOf": 0.5 } } }`) out := AdaptSchema(input, "anthropic") var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) // count: minimum/maximum 应被移除 count := props["count"].(map[string]any) if _, ok := count["minimum"]; ok { t.Error("minimum should be removed for anthropic") } if _, ok := count["maximum"]; ok { t.Error("maximum should be removed for anthropic") } // description 应包含约束信息 desc, _ := count["description"].(string) if !strings.Contains(desc, "minimum=1") || !strings.Contains(desc, "maximum=100") { t.Errorf("description should contain constraint info, got: %q", desc) } if !strings.Contains(desc, "number of items") { t.Errorf("original description should be preserved, got: %q", desc) } // ratio: multipleOf 应被移除,description 是新建的约束说明 ratio := props["ratio"].(map[string]any) if _, ok := ratio["multipleOf"]; ok { t.Error("multipleOf should be removed for anthropic") } ratioDesc, _ := ratio["description"].(string) if !strings.Contains(ratioDesc, "multipleOf=0.5") { t.Errorf("ratio description should contain multipleOf info, got: %q", ratioDesc) } } // TestAdaptSchema_AnthropicRemovesStringConstraints 验证 minLength/maxLength/pattern 被移除. func TestAdaptSchema_AnthropicRemovesStringConstraints(t *testing.T) { input := json.RawMessage(`{ "type": "object", "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 100, "pattern": "^[a-z]+$" } } }`) out := AdaptSchema(input, "anthropic") var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) name := props["name"].(map[string]any) for _, key := range []string{"minLength", "maxLength", "pattern"} { if _, ok := name[key]; ok { t.Errorf("%s should be removed for anthropic", key) } } // 约束信息应写入 description desc, _ := name["description"].(string) if !strings.Contains(desc, "minLength=1") { t.Errorf("description missing minLength, got: %q", desc) } } // TestAdaptSchema_AnthropicMinItemsCoercion 验证 minItems > 1 被降为 1,原值写入 description. func TestAdaptSchema_AnthropicMinItemsCoercion(t *testing.T) { input := json.RawMessage(`{ "type": "array", "items": {"type": "string"}, "minItems": 3 }`) out := AdaptSchema(input, "anthropic") var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } // minItems 应被降为 1 if mi, ok := outObj["minItems"]; !ok || mi.(float64) != 1 { t.Errorf("minItems should be coerced to 1, got: %v", outObj["minItems"]) } // 原值应写入 description desc, _ := outObj["description"].(string) if !strings.Contains(desc, "minItems=3") { t.Errorf("description should contain original minItems, got: %q", desc) } } // TestAdaptSchema_AnthropicMinItemsZeroPreserved 验证 minItems=0 不变(合法 Anthropic 语义). func TestAdaptSchema_AnthropicMinItemsZeroPreserved(t *testing.T) { input := json.RawMessage(`{ "type": "array", "items": {"type": "string"}, "minItems": 0 }`) out := AdaptSchema(input, "anthropic") var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } if mi, ok := outObj["minItems"]; !ok || mi.(float64) != 0 { t.Errorf("minItems=0 should be preserved, got: %v", outObj["minItems"]) } } // TestAdaptSchema_OpenAIStrictRemovesAllOf 验证 openai-strict 路径移除 allOf/not/if/then/else. func TestAdaptSchema_OpenAIStrictRemovesAllOf(t *testing.T) { input := json.RawMessage(`{ "type": "object", "properties": { "value": { "type": "integer", "allOf": [{"minimum": 0}], "not": {"maximum": -1}, "if": {"properties": {"type": {"const": "special"}}}, "then": {"required": ["extra"]}, "else": {} } } }`) out := AdaptSchema(input, "openai-strict") var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) value := props["value"].(map[string]any) for _, key := range []string{"allOf", "not", "if", "then", "else"} { if _, ok := value[key]; ok { t.Errorf("%s should be removed for openai-strict", key) } } // type 字段应保留 if value["type"] != "integer" { t.Errorf("type should be preserved, got: %v", value["type"]) } } // TestAdaptSchema_PassthroughProviders 验证其他 provider(minimax/openrouter/openai/gemini)原样返回. func TestAdaptSchema_PassthroughProviders(t *testing.T) { input := json.RawMessage(`{"type":"object","properties":{"x":{"type":"integer","minimum":1,"maximum":100}}}`) for _, p := range []string{"minimax", "openrouter", "openai", "gemini", ""} { out := AdaptSchema(input, p) var inObj, outObj any _ = json.Unmarshal(input, &inObj) _ = json.Unmarshal(out, &outObj) inJ, _ := json.Marshal(inObj) outJ, _ := json.Marshal(outObj) if string(inJ) != string(outJ) { t.Errorf("provider %q: schema should be unchanged\n in: %s\n out: %s", p, inJ, outJ) } } } // TestAdaptSchema_Empty 验证空输入原样返回. func TestAdaptSchema_Empty(t *testing.T) { out := AdaptSchema(nil, "anthropic") if len(out) != 0 { t.Errorf("expected empty, got %s", out) } } // TestAdaptSchema_AnthropicNestedRecursive 验证约束裁剪递归处理嵌套 schema. func TestAdaptSchema_AnthropicNestedRecursive(t *testing.T) { input := json.RawMessage(`{ "type": "object", "properties": { "nested": { "type": "object", "properties": { "deep": { "type": "number", "minimum": 5 } } } } }`) out := AdaptSchema(input, "anthropic") var outObj map[string]any if err := json.Unmarshal(out, &outObj); err != nil { t.Fatalf("unmarshal: %v", err) } props := outObj["properties"].(map[string]any) nested := props["nested"].(map[string]any) nestedProps := nested["properties"].(map[string]any) deep := nestedProps["deep"].(map[string]any) if _, ok := deep["minimum"]; ok { t.Error("nested minimum should be removed for anthropic") } desc, _ := deep["description"].(string) if !strings.Contains(desc, "minimum=5") { t.Errorf("nested description should contain constraint info, got: %q", desc) } } // ───────────────────────────────────────────────────────────────────────────── // CAP-8: ValidateSchemaComplexity 测试 // ───────────────────────────────────────────────────────────────────────────── // TestValidateSchemaComplexity_NestingDepthOK 验证嵌套深度 ≤ 10 时返回 nil. func TestValidateSchemaComplexity_NestingDepthOK(t *testing.T) { // 使用一个扁平的单层 schema(不超出深度限制) schema := json.RawMessage(`{"type":"object","properties":{"a":{"type":"string"},"b":{"type":"integer"}}}`) if err := ValidateSchemaComplexity(schema, "openai-strict"); err != nil { t.Errorf("expected nil, got: %v", err) } } // TestValidateSchemaComplexity_NestingDepthExceeds 验证嵌套深度 > 10 返回 SchemaComplexityError. func TestValidateSchemaComplexity_NestingDepthExceeds(t *testing.T) { // 构造 12 层嵌套(超过 openAIMaxNestingDepth=10) schema := buildNestedSchema(12) err := ValidateSchemaComplexity(schema, "openai-strict") if err == nil { t.Fatal("expected error for nesting depth > 10") } var sce *SchemaComplexityError if !errors.As(err, &sce) { t.Fatalf("expected *SchemaComplexityError, got: %T", err) } if sce.Dimension != "nesting_depth" { t.Errorf("expected dimension=nesting_depth, got: %s", sce.Dimension) } if sce.Actual <= 10 { t.Errorf("expected actual > 10, got: %d", sce.Actual) } if sce.Limit != openAIMaxNestingDepth { t.Errorf("expected limit=%d, got: %d", openAIMaxNestingDepth, sce.Limit) } // 错误消息应包含维度,实际值,上限 msg := err.Error() if !strings.Contains(msg, "nesting_depth") || !strings.Contains(msg, "10") { t.Errorf("error message should contain 'nesting_depth' and '10', got: %s", msg) } } // TestValidateSchemaComplexity_TotalPropertiesExceeds 验证总属性数 > 5000 返回错误. func TestValidateSchemaComplexity_TotalPropertiesExceeds(t *testing.T) { // 构造一个 properties 字段数超过 5000 的 schema(不嵌套,避免触发 depth check) props := make(map[string]any) for i := 0; i < 5001; i++ { key := fmt.Sprintf("field%d", i) props[key] = map[string]string{"type": "string"} } raw := map[string]any{"type": "object", "properties": props} schema, _ := json.Marshal(raw) err := ValidateSchemaComplexity(schema, "openai-strict") if err == nil { t.Fatal("expected error for total_properties > 5000") } var sce *SchemaComplexityError if !errors.As(err, &sce) { t.Fatalf("expected *SchemaComplexityError, got: %T", err) } if sce.Dimension != "total_properties" { t.Errorf("expected dimension=total_properties, got: %s", sce.Dimension) } if sce.Actual <= 5000 { t.Errorf("expected actual > 5000, got: %d", sce.Actual) } } // TestValidateSchemaComplexity_EnumValuesExceeds 验证 enum 值数 > 1000 返回错误. func TestValidateSchemaComplexity_EnumValuesExceeds(t *testing.T) { enumVals := make([]string, 1001) for i := range enumVals { enumVals[i] = fmt.Sprintf("val%d", i) } raw := map[string]any{ "type": "object", "properties": map[string]any{ "status": map[string]any{ "type": "string", "enum": enumVals, }, }, } schema, _ := json.Marshal(raw) err := ValidateSchemaComplexity(schema, "openai-strict") if err == nil { t.Fatal("expected error for enum_values > 1000") } var sce *SchemaComplexityError if !errors.As(err, &sce) { t.Fatalf("expected *SchemaComplexityError, got: %T", err) } if sce.Dimension != "enum_values" { t.Errorf("expected dimension=enum_values, got: %s", sce.Dimension) } if sce.Actual != 1001 { t.Errorf("expected actual=1001, got: %d", sce.Actual) } if sce.Limit != openAIMaxEnumValues { t.Errorf("expected limit=%d, got: %d", openAIMaxEnumValues, sce.Limit) } } // TestValidateSchemaComplexity_OtherProviders 验证非 openai-strict provider 始终返回 nil. func TestValidateSchemaComplexity_OtherProviders(t *testing.T) { // 构造一个明显超限的 schema(深嵌套 + 大 enum) deepSchema := buildNestedSchema(20) for _, p := range []string{"anthropic", "openai", "minimax", "openrouter", "gemini", ""} { if err := ValidateSchemaComplexity(deepSchema, p); err != nil { t.Errorf("provider %q: expected nil, got: %v", p, err) } } } // TestValidateSchemaComplexity_Empty 验证空 schema 返回 nil. func TestValidateSchemaComplexity_Empty(t *testing.T) { if err := ValidateSchemaComplexity(nil, "openai-strict"); err != nil { t.Errorf("expected nil for empty schema, got: %v", err) } } // TestValidateSchemaComplexity_BoundaryDepth 验证边界值:深度恰好等于 openAIMaxNestingDepth 时不报错. // 使用一个简单的扁平 schema 确保不超过限制. func TestValidateSchemaComplexity_BoundaryDepth(t *testing.T) { // 3 层嵌套:outer{properties{child{type:string}}} - 原始 JSON 对象深度 = 3 schema := json.RawMessage(`{"type":"object","properties":{"child":{"type":"object","properties":{"leaf":{"type":"string"}}}}}`) if err := ValidateSchemaComplexity(schema, "openai-strict"); err != nil { t.Errorf("shallow schema should pass, got: %v", err) } } // TestValidateSchemaComplexity_BoundaryEnum 验证边界值:enum 恰好 1000 个时不报错. func TestValidateSchemaComplexity_BoundaryEnum(t *testing.T) { enumVals := make([]string, 1000) for i := range enumVals { enumVals[i] = fmt.Sprintf("v%d", i) } raw := map[string]any{ "type": "string", "enum": enumVals, } schema, _ := json.Marshal(raw) if err := ValidateSchemaComplexity(schema, "openai-strict"); err != nil { t.Errorf("enum=1000 should pass, got: %v", err) } } // buildNestedSchema 构造指定层数的嵌套 JSON Schema(每层有一个 properties.child). // 用于嵌套深度测试. func buildNestedSchema(depth int) json.RawMessage { if depth <= 0 { raw, _ := json.Marshal(map[string]string{"type": "string"}) return raw } inner := buildNestedSchema(depth - 1) var innerObj any _ = json.Unmarshal(inner, &innerObj) obj := map[string]any{ "type": "object", "properties": map[string]any{ "child": innerObj, }, } raw, _ := json.Marshal(obj) return raw } // contains 是测试辅助函数:检查 JSON 字节序列中是否包含子串. func contains(raw json.RawMessage, sub string) bool { return len(raw) > 0 && indexBytes(raw, []byte(sub)) >= 0 } func indexBytes(s, sep []byte) int { n := len(sep) if n == 0 { return 0 } for i := 0; i+n <= len(s); i++ { if string(s[i:i+n]) == string(sep) { return i } } return -1 }