返回 DeepSeek-Reasonix
schema_canonicalize_test.go
根目录 / internal / provider / schema_canonicalize_test.go
1 package provider
2
3 import (
4 "encoding/json"
5 "testing"
6 )
7
8 func TestCanonicalizeSchemaDropsNonArrayRequired(t *testing.T) {
9 raw := json.RawMessage(`{
10 "type":"object",
11 "properties":{
12 "query":{"type":"string","required":true},
13 "nested":{"type":"object","required":false,"properties":{"x":{"type":"string"}}}
14 },
15 "required":["query","nested"]
16 }`)
17
18 got := string(CanonicalizeSchema(raw))
19 want := `{"properties":{"nested":{"properties":{"x":{"type":"string"}},"type":"object"},"query":{"type":"string"}},"required":["nested","query"],"type":"object"}`
20 if got != want {
21 t.Fatalf("CanonicalizeSchema() = %s, want %s", got, want)
22 }
23 }
24
25 func TestCanonicalizeSchemaAddsEmptyPropertiesForNoArgumentObject(t *testing.T) {
26 for _, raw := range []json.RawMessage{
27 nil,
28 json.RawMessage(`null`),
29 json.RawMessage(`{"type":"object"}`),
30 } {
31 got := string(CanonicalizeSchema(raw))
32 want := `{"properties":{},"type":"object"}`
33 if got != want {
34 t.Fatalf("CanonicalizeSchema(%s) = %s, want %s", string(raw), got, want)
35 }
36 }
37 }
38
39 func TestCanonicalizeSchemaAddsMissingRootType(t *testing.T) {
40 for _, tc := range []struct{ raw, want string }{
41 {`{}`, `{"properties":{},"type":"object"}`},
42 {`{"properties":{"q":{"type":"string"}}}`, `{"properties":{"q":{"type":"string"}},"type":"object"}`},
43 // Explicit non-object root types are preserved verbatim; validation
44 // quarantines them instead of silently rewriting declared semantics.
45 {`{"type":"string"}`, `{"type":"string"}`},
46 {`{"type":["object","null"]}`, `{"type":["object","null"]}`},
47 } {
48 if got := string(CanonicalizeSchema(json.RawMessage(tc.raw))); got != tc.want {
49 t.Fatalf("CanonicalizeSchema(%s) = %s, want %s", tc.raw, got, tc.want)
50 }
51 }
52 }
53
54 func TestCanonicalizeSchemaPreservesPropertyNamedRequired(t *testing.T) {
55 raw := json.RawMessage(`{
56 "type":"object",
57 "properties":{
58 "required":{"type":"boolean","description":"Whether the item is required"},
59 "normal":{"type":"string"}
60 },
61 "required":["required"]
62 }`)
63
64 got := string(CanonicalizeSchema(raw))
65 want := `{"properties":{"normal":{"type":"string"},"required":{"description":"Whether the item is required","type":"boolean"}},"required":["required"],"type":"object"}`
66 if got != want {
67 t.Fatalf("CanonicalizeSchema() = %s, want %s", got, want)
68 }
69 }
70
71 func TestCanonicalizeSchemaDependentRequired(t *testing.T) {
72 raw := json.RawMessage(`{
73 "type":"object",
74 "dependentRequired":{
75 "cc":["billing_address","name"],
76 "bad":true
77 }
78 }`)
79
80 got := string(CanonicalizeSchema(raw))
81 want := `{"dependentRequired":{"cc":["billing_address","name"]},"properties":{},"type":"object"}`
82 if got != want {
83 t.Fatalf("CanonicalizeSchema() = %s, want %s", got, want)
84 }
85 }
86
87 func TestCanonicalizeSchemaPreservesDependentRequiredPropertyName(t *testing.T) {
88 raw := json.RawMessage(`{
89 "type":"object",
90 "properties":{
91 "dependentRequired":{"type":"string"},
92 "x":{"type":"string"}
93 },
94 "dependentRequired":{
95 "dependentRequired":["x"]
96 }
97 }`)
98
99 got := string(CanonicalizeSchema(raw))
100 want := `{"dependentRequired":{"dependentRequired":["x"]},"properties":{"dependentRequired":{"type":"string"},"x":{"type":"string"}},"type":"object"}`
101 if got != want {
102 t.Fatalf("CanonicalizeSchema() = %s, want %s", got, want)
103 }
104 }
105
106 func TestNormalizeLegacyTupleItemsForDraft202012(t *testing.T) {
107 raw := json.RawMessage(`{
108 "type":"object",
109 "properties":{
110 "pair":{
111 "type":"array",
112 "items":[{"type":"string"},{"type":"number"}],
113 "additionalItems":false
114 }
115 }
116 }`)
117
118 got := string(NormalizeLegacyTupleItemsForDraft202012(raw))
119 want := `{"properties":{"pair":{"items":false,"prefixItems":[{"type":"string"},{"type":"number"}],"type":"array"}},"type":"object"}`
120 if got != want {
121 t.Fatalf("NormalizeLegacyTupleItemsForDraft202012() = %s, want %s", got, want)
122 }
123 if again := string(NormalizeLegacyTupleItemsForDraft202012(json.RawMessage(got))); again != got {
124 t.Fatalf("tuple migration is not idempotent:\n first: %s\nsecond: %s", got, again)
125 }
126 }
127
128 func TestNormalizeLegacyTupleItemsForDraft202012Nested(t *testing.T) {
129 raw := json.RawMessage(`{
130 "type":"object",
131 "$defs":{
132 "value":{
133 "anyOf":[
134 {"type":"string"},
135 {"type":"array","items":[{"type":"string"},{"type":"number"}]}
136 ]
137 }
138 },
139 "properties":{"value":{"$ref":"#/$defs/value"}}
140 }`)
141
142 got := string(NormalizeLegacyTupleItemsForDraft202012(raw))
143 want := `{"$defs":{"value":{"anyOf":[{"type":"string"},{"prefixItems":[{"type":"string"},{"type":"number"}],"type":"array"}]}},"properties":{"value":{"$ref":"#/$defs/value"}},"type":"object"}`
144 if got != want {
145 t.Fatalf("NormalizeLegacyTupleItemsForDraft202012() = %s, want %s", got, want)
146 }
147 }
148
149 func TestNormalizeLegacyTupleItemsPreservesExistingPrefixItems(t *testing.T) {
150 raw := json.RawMessage(`{
151 "type":"array",
152 "prefixItems":[{"type":"boolean"}],
153 "items":[{"type":"string"}],
154 "additionalItems":{"type":"number"}
155 }`)
156
157 got := string(NormalizeLegacyTupleItemsForDraft202012(raw))
158 want := `{"items":{"type":"number"},"prefixItems":[{"type":"boolean"}],"type":"array"}`
159 if got != want {
160 t.Fatalf("NormalizeLegacyTupleItemsForDraft202012() = %s, want %s", got, want)
161 }
162 }
163
164 func TestNormalizeLegacyTupleItemsPreservesModernArrayItems(t *testing.T) {
165 // Modern object-form items need no rewrite, so the input bytes come back
166 // exactly — not a reserialized (key-sorted) copy, which would perturb the
167 // canonical bytes the registry produced once.
168 raw := json.RawMessage(`{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}`)
169 if got := string(NormalizeLegacyTupleItemsForDraft202012(raw)); got != string(raw) {
170 t.Fatalf("NormalizeLegacyTupleItemsForDraft202012() = %s, want the input bytes unchanged %s", got, raw)
171 }
172 }
173
174 func TestCanonicalizeSchemaPreservesLegacyTupleItems(t *testing.T) {
175 raw := json.RawMessage(`{"type":"object","properties":{"pair":{"type":"array","items":[{"type":"string"},{"type":"number"}],"additionalItems":false}}}`)
176 want := `{"properties":{"pair":{"additionalItems":false,"items":[{"type":"string"},{"type":"number"}],"type":"array"}},"type":"object"}`
177 if got := string(CanonicalizeSchema(raw)); got != want {
178 t.Fatalf("CanonicalizeSchema() = %s, want provider-neutral bytes %s", got, want)
179 }
180 }
181
181 lines GO