1- package latest
1+ package config
22
33import (
44 "encoding/json"
55 "maps"
66 "os"
77 "reflect"
8- "sort"
98 "strings"
109 "testing"
1110
11+ "github.com/goccy/go-yaml"
1212 "github.com/stretchr/testify/assert"
1313 "github.com/stretchr/testify/require"
14+ "github.com/xeipuuv/gojsonschema"
15+
16+ "github.com/docker/cagent/pkg/config/latest"
1417)
1518
1619// schemaFile is the path to the JSON schema file relative to the repo root.
17- const schemaFile = "../../../ agent-schema.json"
20+ const schemaFile = "../../agent-schema.json"
1821
19- // jsonSchema mirrors the subset of JSON Schema we need for comparison.
20- type jsonSchema struct {
21- Properties map [string ]jsonSchema `json:"properties,omitempty"`
22- Definitions map [string ]jsonSchema `json:"definitions,omitempty"`
23- Ref string `json:"$ref,omitempty"`
24- Items * jsonSchema `json:"items,omitempty"`
25- AdditionalProperties any `json:"additionalProperties,omitempty"`
26- }
22+ func TestJsonSchemaWorksForExamples (t * testing.T ) {
23+ schemaFile , err := os .ReadFile (schemaFile )
24+ require .NoError (t , err )
2725
28- // resolveRef follows a $ref like "#/definitions/Foo" and returns the
29- // referenced schema. When no $ref is present it returns the receiver unchanged.
30- func (s jsonSchema ) resolveRef (root jsonSchema ) jsonSchema {
31- if s .Ref == "" {
32- return s
33- }
34- const prefix = "#/definitions/"
35- if ! strings .HasPrefix (s .Ref , prefix ) {
36- return s
37- }
38- name := strings .TrimPrefix (s .Ref , prefix )
39- if def , ok := root .Definitions [name ]; ok {
40- return def
41- }
42- return s
43- }
26+ schema , err := gojsonschema .NewSchema (gojsonschema .NewBytesLoader (schemaFile ))
27+ require .NoError (t , err )
4428
45- // structJSONFields returns the set of JSON property names declared on a Go
46- // struct type via `json:"<name>,…"` tags. Fields tagged with `json:"-"` are
47- // excluded. It recurses into anonymous (embedded) struct fields so that
48- // promoted fields are included.
49- func structJSONFields (t reflect.Type ) map [string ]bool {
50- if t .Kind () == reflect .Pointer {
51- t = t .Elem ()
52- }
53- fields := make (map [string ]bool )
54- for f := range t .Fields () {
55- // Recurse into anonymous (embedded) structs.
56- if f .Anonymous {
57- maps .Copy (fields , structJSONFields (f .Type ))
58- continue
59- }
29+ for _ , file := range collectExamples (t ) {
30+ t .Run (file , func (t * testing.T ) {
31+ t .Parallel ()
6032
61- tag := f .Tag .Get ("json" )
62- if tag == "" || tag == "-" {
63- continue
64- }
65- name , _ , _ := strings .Cut (tag , "," )
66- if name != "" && name != "-" {
67- fields [name ] = true
68- }
69- }
70- return fields
71- }
33+ buf , err := os .ReadFile (file )
34+ require .NoError (t , err )
7235
73- // schemaProperties returns the set of property names from a JSON schema
74- // definition. It does NOT follow $ref on individual properties – it only
75- // looks at the top-level "properties" map.
76- func schemaProperties (def jsonSchema ) map [string ]bool {
77- props := make (map [string ]bool , len (def .Properties ))
78- for k := range def .Properties {
79- props [k ] = true
80- }
81- return props
82- }
36+ var rawJSON any
37+ err = yaml .Unmarshal (buf , & rawJSON )
38+ require .NoError (t , err )
8339
84- func sortedKeys ( m map [ string ] bool ) [] string {
85- keys := make ([] string , 0 , len ( m ) )
86- for k := range m {
87- keys = append ( keys , k )
40+ result , err := schema . Validate ( gojsonschema . NewRawLoader ( rawJSON ))
41+ require . NoError ( t , err )
42+ assert . True ( t , result . Valid (), "Example %s does not match schema: %v" , file , result . Errors ())
43+ } )
8844 }
89- sort .Strings (keys )
90- return keys
9145}
9246
9347// TestSchemaMatchesGoTypes verifies that every JSON-tagged field in the Go
@@ -116,27 +70,27 @@ func TestSchemaMatchesGoTypes(t *testing.T) {
11670
11771 entries := []entry {
11872 // Top-level Config
119- {reflect .TypeFor [Config ](), root , "Config (top-level)" },
73+ {reflect .TypeFor [latest. Config ](), root , "Config (top-level)" },
12074 }
12175
12276 // Definitions that map 1:1 to a Go struct.
12377 definitionMap := map [string ]reflect.Type {
124- "AgentConfig" : reflect .TypeFor [AgentConfig ](),
125- "FallbackConfig" : reflect .TypeFor [FallbackConfig ](),
126- "ModelConfig" : reflect .TypeFor [ModelConfig ](),
127- "Metadata" : reflect .TypeFor [Metadata ](),
128- "ProviderConfig" : reflect .TypeFor [ProviderConfig ](),
129- "Toolset" : reflect .TypeFor [Toolset ](),
130- "Remote" : reflect .TypeFor [Remote ](),
131- "SandboxConfig" : reflect .TypeFor [SandboxConfig ](),
132- "ScriptShellToolConfig" : reflect .TypeFor [ScriptShellToolConfig ](),
133- "PostEditConfig" : reflect .TypeFor [PostEditConfig ](),
134- "PermissionsConfig" : reflect .TypeFor [PermissionsConfig ](),
135- "HooksConfig" : reflect .TypeFor [HooksConfig ](),
136- "HookMatcherConfig" : reflect .TypeFor [HookMatcherConfig ](),
137- "HookDefinition" : reflect .TypeFor [HookDefinition ](),
138- "RoutingRule" : reflect .TypeFor [RoutingRule ](),
139- "ApiConfig" : reflect .TypeFor [APIToolConfig ](),
78+ "AgentConfig" : reflect .TypeFor [latest. AgentConfig ](),
79+ "FallbackConfig" : reflect .TypeFor [latest. FallbackConfig ](),
80+ "ModelConfig" : reflect .TypeFor [latest. ModelConfig ](),
81+ "Metadata" : reflect .TypeFor [latest. Metadata ](),
82+ "ProviderConfig" : reflect .TypeFor [latest. ProviderConfig ](),
83+ "Toolset" : reflect .TypeFor [latest. Toolset ](),
84+ "Remote" : reflect .TypeFor [latest. Remote ](),
85+ "SandboxConfig" : reflect .TypeFor [latest. SandboxConfig ](),
86+ "ScriptShellToolConfig" : reflect .TypeFor [latest. ScriptShellToolConfig ](),
87+ "PostEditConfig" : reflect .TypeFor [latest. PostEditConfig ](),
88+ "PermissionsConfig" : reflect .TypeFor [latest. PermissionsConfig ](),
89+ "HooksConfig" : reflect .TypeFor [latest. HooksConfig ](),
90+ "HookMatcherConfig" : reflect .TypeFor [latest. HookMatcherConfig ](),
91+ "HookDefinition" : reflect .TypeFor [latest. HookDefinition ](),
92+ "RoutingRule" : reflect .TypeFor [latest. RoutingRule ](),
93+ "ApiConfig" : reflect .TypeFor [latest. APIToolConfig ](),
14094 }
14195
14296 for name , goType := range definitionMap {
@@ -156,13 +110,13 @@ func TestSchemaMatchesGoTypes(t *testing.T) {
156110 }
157111
158112 inlines := []inlineEntry {
159- {reflect .TypeFor [StructuredOutput ](), []string {"AgentConfig" , "structured_output" }, "StructuredOutput (AgentConfig.structured_output)" },
160- {reflect .TypeFor [RAGConfig ](), []string {"RAGConfig" }, "RAGConfig" },
161- {reflect .TypeFor [RAGToolConfig ](), []string {"RAGConfig" , "tool" }, "RAGToolConfig (RAGConfig.tool)" },
162- {reflect .TypeFor [RAGResultsConfig ](), []string {"RAGConfig" , "results" }, "RAGResultsConfig (RAGConfig.results)" },
163- {reflect .TypeFor [RAGFusionConfig ](), []string {"RAGConfig" , "results" , "fusion" }, "RAGFusionConfig (RAGConfig.results.fusion)" },
164- {reflect .TypeFor [RAGRerankingConfig ](), []string {"RAGConfig" , "results" , "reranking" }, "RAGRerankingConfig (RAGConfig.results.reranking)" },
165- {reflect .TypeFor [RAGChunkingConfig ](), []string {"RAGConfig" , "strategies" , "*" , "chunking" }, "RAGChunkingConfig (RAGConfig.strategies[].chunking)" },
113+ {reflect .TypeFor [latest. StructuredOutput ](), []string {"AgentConfig" , "structured_output" }, "StructuredOutput (AgentConfig.structured_output)" },
114+ {reflect .TypeFor [latest. RAGConfig ](), []string {"RAGConfig" }, "RAGConfig" },
115+ {reflect .TypeFor [latest. RAGToolConfig ](), []string {"RAGConfig" , "tool" }, "RAGToolConfig (RAGConfig.tool)" },
116+ {reflect .TypeFor [latest. RAGResultsConfig ](), []string {"RAGConfig" , "results" }, "RAGResultsConfig (RAGConfig.results)" },
117+ {reflect .TypeFor [latest. RAGFusionConfig ](), []string {"RAGConfig" , "results" , "fusion" }, "RAGFusionConfig (RAGConfig.results.fusion)" },
118+ {reflect .TypeFor [latest. RAGRerankingConfig ](), []string {"RAGConfig" , "results" , "reranking" }, "RAGRerankingConfig (RAGConfig.results.reranking)" },
119+ {reflect .TypeFor [latest. RAGChunkingConfig ](), []string {"RAGConfig" , "strategies" , "*" , "chunking" }, "RAGChunkingConfig (RAGConfig.strategies[].chunking)" },
166120 }
167121
168122 for _ , il := range inlines {
@@ -185,6 +139,71 @@ func TestSchemaMatchesGoTypes(t *testing.T) {
185139 }
186140}
187141
142+ // jsonSchema mirrors the subset of JSON Schema we need for comparison.
143+ type jsonSchema struct {
144+ Properties map [string ]jsonSchema `json:"properties,omitempty"`
145+ Definitions map [string ]jsonSchema `json:"definitions,omitempty"`
146+ Ref string `json:"$ref,omitempty"`
147+ Items * jsonSchema `json:"items,omitempty"`
148+ AdditionalProperties any `json:"additionalProperties,omitempty"`
149+ }
150+
151+ // resolveRef follows a $ref like "#/definitions/Foo" and returns the
152+ // referenced schema. When no $ref is present it returns the receiver unchanged.
153+ func (s jsonSchema ) resolveRef (root jsonSchema ) jsonSchema {
154+ if s .Ref == "" {
155+ return s
156+ }
157+ const prefix = "#/definitions/"
158+ if ! strings .HasPrefix (s .Ref , prefix ) {
159+ return s
160+ }
161+ name := strings .TrimPrefix (s .Ref , prefix )
162+ if def , ok := root .Definitions [name ]; ok {
163+ return def
164+ }
165+ return s
166+ }
167+
168+ // structJSONFields returns the set of JSON property names declared on a Go
169+ // struct type via `json:"<name>,…"` tags. Fields tagged with `json:"-"` are
170+ // excluded. It recurses into anonymous (embedded) struct fields so that
171+ // promoted fields are included.
172+ func structJSONFields (t reflect.Type ) map [string ]bool {
173+ if t .Kind () == reflect .Pointer {
174+ t = t .Elem ()
175+ }
176+ fields := make (map [string ]bool )
177+ for f := range t .Fields () {
178+ // Recurse into anonymous (embedded) structs.
179+ if f .Anonymous {
180+ maps .Copy (fields , structJSONFields (f .Type ))
181+ continue
182+ }
183+
184+ tag := f .Tag .Get ("json" )
185+ if tag == "" || tag == "-" {
186+ continue
187+ }
188+ name , _ , _ := strings .Cut (tag , "," )
189+ if name != "" && name != "-" {
190+ fields [name ] = true
191+ }
192+ }
193+ return fields
194+ }
195+
196+ // schemaProperties returns the set of property names from a JSON schema
197+ // definition. It does NOT follow $ref on individual properties – it only
198+ // looks at the top-level "properties" map.
199+ func schemaProperties (def jsonSchema ) map [string ]bool {
200+ props := make (map [string ]bool , len (def .Properties ))
201+ for k := range def .Properties {
202+ props [k ] = true
203+ }
204+ return props
205+ }
206+
188207// navigateSchema walks from a top-level definition through nested properties.
189208// path[0] is the definition name; subsequent elements are property names.
190209// The special element "*" dereferences an array's "items" schema.
0 commit comments