Skip to content

Commit 76a05f0

Browse files
authored
Merge pull request #1857 from dgageot/schematest
Schema tests should be only for latest version
2 parents 9817e64 + 553121f commit 76a05f0

File tree

4 files changed

+112
-571
lines changed

4 files changed

+112
-571
lines changed

pkg/config/examples_test.go

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ package config
22

33
import (
44
"io/fs"
5-
"os"
65
"path/filepath"
76
"testing"
87

98
"github.com/goccy/go-yaml"
109
"github.com/stretchr/testify/assert"
1110
"github.com/stretchr/testify/require"
12-
"github.com/xeipuuv/gojsonschema"
1311

1412
"github.com/docker/cagent/pkg/config/latest"
1513
"github.com/docker/cagent/pkg/modelsdev"
@@ -78,32 +76,6 @@ func TestParseExamples(t *testing.T) {
7876
}
7977
}
8078

81-
func TestJsonSchemaWorksForExamples(t *testing.T) {
82-
// Read json schema.
83-
schemaFile, err := os.ReadFile(filepath.Join("..", "..", "agent-schema.json"))
84-
require.NoError(t, err)
85-
86-
schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(schemaFile))
87-
require.NoError(t, err)
88-
89-
for _, file := range collectExamples(t) {
90-
t.Run(file, func(t *testing.T) {
91-
t.Parallel()
92-
93-
buf, err := os.ReadFile(file)
94-
require.NoError(t, err)
95-
96-
var rawJSON any
97-
err = yaml.Unmarshal(buf, &rawJSON)
98-
require.NoError(t, err)
99-
100-
result, err := schema.Validate(gojsonschema.NewRawLoader(rawJSON))
101-
require.NoError(t, err)
102-
assert.True(t, result.Valid(), "Example %s does not match schema: %v", file, result.Errors())
103-
})
104-
}
105-
}
106-
10779
func TestParseExamplesAfterMarshalling(t *testing.T) {
10880
for _, file := range collectExamples(t) {
10981
t.Run(file, func(t *testing.T) {
Lines changed: 112 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,47 @@
1-
package latest
1+
package config
22

33
import (
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

Comments
 (0)