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