181 lines
7.4 KiB
Go
181 lines
7.4 KiB
Go
package generatedtext
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptassets"
|
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
)
|
|
|
|
func TestGeneratedTextValidatorsRequireExactObjectFields(t *testing.T) {
|
|
validators := []struct {
|
|
name string
|
|
valid string
|
|
validate func([]byte) error
|
|
}{
|
|
{name: "daily", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateDailyJSON},
|
|
{name: "today", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTodayJSON},
|
|
{name: "tomorrow", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTomorrowJSON},
|
|
{name: "hourly", valid: `{"summary":"Summary","forecast_discussion":"Discussion","precipitation_timing":""}`, validate: validateHourlyJSON},
|
|
}
|
|
for _, validator := range validators {
|
|
t.Run(validator.name, func(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
input string
|
|
valid bool
|
|
}{
|
|
{name: "canonical", input: validator.valid, valid: true},
|
|
{name: "missing", input: strings.Replace(validator.valid, `"summary":"Summary",`, "", 1)},
|
|
{name: "additional", input: strings.Replace(validator.valid, "}", `,"extra":"value"}`, 1)},
|
|
{name: "case variant summary", input: strings.Replace(validator.valid, `"summary"`, `"Summary"`, 1)},
|
|
{name: "case variant discussion", input: strings.Replace(validator.valid, `"forecast_discussion"`, `"Forecast_Discussion"`, 1)},
|
|
{name: "duplicate", input: strings.Replace(validator.valid, `"summary":"Summary",`, `"summary":"Summary","summary":"Other",`, 1)},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := validator.validate([]byte(test.input))
|
|
if (err == nil) != test.valid {
|
|
t.Fatalf("validate(%s) error = %v, want valid = %t", test.input, err, test.valid)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypedValidatorsMatchEmbeddedSchemaObjectShape(t *testing.T) {
|
|
validators := []struct {
|
|
name string
|
|
valid string
|
|
validate func([]byte) error
|
|
}{
|
|
{name: "daily", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateDailyJSON},
|
|
{name: "today", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTodayJSON},
|
|
{name: "tomorrow", valid: `{"summary":"Summary","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTomorrowJSON},
|
|
{name: "hourly", valid: `{"summary":"Summary","forecast_discussion":"Discussion","precipitation_timing":""}`, validate: validateHourlyJSON},
|
|
}
|
|
for _, validator := range validators {
|
|
t.Run(validator.name, func(t *testing.T) {
|
|
schema := generatedTextSchema(t, validator.name)
|
|
for _, test := range []struct {
|
|
name string
|
|
input string
|
|
}{
|
|
{name: "canonical", input: validator.valid},
|
|
{name: "missing", input: strings.Replace(validator.valid, `"summary":"Summary",`, "", 1)},
|
|
{name: "additional", input: strings.Replace(validator.valid, "}", `,"extra":"value"}`, 1)},
|
|
{name: "case variant", input: strings.Replace(validator.valid, `"summary"`, `"Summary"`, 1)},
|
|
{name: "null", input: strings.Replace(validator.valid, `"precipitation_timing":""`, `"precipitation_timing":null`, 1)},
|
|
{name: "wrong type", input: strings.Replace(validator.valid, `"summary":"Summary"`, `"summary":false`, 1)},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
instance, err := jsonschema.UnmarshalJSON(strings.NewReader(test.input))
|
|
if err != nil {
|
|
t.Fatalf("parse schema instance: %v", err)
|
|
}
|
|
schemaValid := schema.Validate(instance) == nil
|
|
typedValid := validator.validate([]byte(test.input)) == nil
|
|
if schemaValid != typedValid {
|
|
t.Fatalf("schema valid = %t, typed valid = %t for %s", schemaValid, typedValid, test.input)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypedValidatorsMatchEmbeddedSchemaContentLimits(t *testing.T) {
|
|
validators := []struct {
|
|
name string
|
|
valid string
|
|
invalid []string
|
|
validate func([]byte) error
|
|
}{
|
|
{
|
|
name: "daily",
|
|
valid: fmt.Sprintf(`{"summary":%q,"forecast_discussion":[%q],"precipitation_timing":""}`, strings.Repeat("s", 4_000), strings.Repeat("d", 4_000)),
|
|
invalid: []string{fmt.Sprintf(`{"summary":%q,"forecast_discussion":["discussion"],"precipitation_timing":""}`, strings.Repeat("s", 4_001)), fmt.Sprintf(`{"summary":"summary","forecast_discussion":[%s],"precipitation_timing":""}`, strings.TrimSuffix(strings.Repeat(`"discussion",`, 13), ",")), `{"summary":"summary","forecast_discussion":[null],"precipitation_timing":""}`},
|
|
validate: validateDailyJSON,
|
|
},
|
|
{
|
|
name: "today",
|
|
valid: fmt.Sprintf(`{"summary":%q,"forecast_discussion":[%q],"precipitation_timing":""}`, strings.Repeat("s", 4_000), strings.Repeat("d", 4_000)),
|
|
invalid: []string{fmt.Sprintf(`{"summary":"summary","forecast_discussion":[%q],"precipitation_timing":""}`, strings.Repeat("d", 4_001))},
|
|
validate: validateTodayJSON,
|
|
},
|
|
{
|
|
name: "tomorrow",
|
|
valid: fmt.Sprintf(`{"summary":%q,"forecast_discussion":[%q],"precipitation_timing":""}`, strings.Repeat("s", 4_000), strings.Repeat("d", 4_000)),
|
|
invalid: []string{fmt.Sprintf(`{"summary":"summary","forecast_discussion":["discussion"],"precipitation_timing":%q}`, strings.Repeat("t", 4_001))},
|
|
validate: validateTomorrowJSON,
|
|
},
|
|
{
|
|
name: "hourly",
|
|
valid: fmt.Sprintf(`{"summary":%q,"forecast_discussion":%q,"precipitation_timing":""}`, strings.Repeat("s", 4_000), strings.Repeat("d", 12_000)),
|
|
invalid: []string{fmt.Sprintf(`{"summary":"summary","forecast_discussion":%q,"precipitation_timing":""}`, strings.Repeat("d", 12_001))},
|
|
validate: validateHourlyJSON,
|
|
},
|
|
}
|
|
for _, validator := range validators {
|
|
t.Run(validator.name, func(t *testing.T) {
|
|
schema := generatedTextSchema(t, validator.name)
|
|
for _, input := range append([]string{validator.valid}, validator.invalid...) {
|
|
instance, err := jsonschema.UnmarshalJSON(strings.NewReader(input))
|
|
if err != nil {
|
|
t.Fatalf("parse schema instance: %v", err)
|
|
}
|
|
schemaValid := schema.Validate(instance) == nil
|
|
typedValid := validator.validate([]byte(input)) == nil
|
|
if schemaValid != typedValid {
|
|
t.Fatalf("schema valid = %t, typed valid = %t", schemaValid, typedValid)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func generatedTextSchema(t *testing.T, id string) *jsonschema.Schema {
|
|
t.Helper()
|
|
data, err := promptassets.Schema(id)
|
|
if err != nil {
|
|
t.Fatalf("Schema(%q) error = %v", id, err)
|
|
}
|
|
document, err := jsonschema.UnmarshalJSON(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("parse schema %q: %v", id, err)
|
|
}
|
|
compiler := jsonschema.NewCompiler()
|
|
location := fmt.Sprintf("https://weatherreporter.example/schemas/%s.json", id)
|
|
if err := compiler.AddResource(location, document); err != nil {
|
|
t.Fatalf("add schema %q: %v", id, err)
|
|
}
|
|
schema, err := compiler.Compile(location)
|
|
if err != nil {
|
|
t.Fatalf("compile schema %q: %v", id, err)
|
|
}
|
|
return schema
|
|
}
|
|
|
|
func validateDailyJSON(data []byte) error {
|
|
_, err := ValidateDaily(data)
|
|
return err
|
|
}
|
|
|
|
func validateTodayJSON(data []byte) error {
|
|
_, err := ValidateToday(data)
|
|
return err
|
|
}
|
|
|
|
func validateTomorrowJSON(data []byte) error {
|
|
_, err := ValidateTomorrow(data)
|
|
return err
|
|
}
|
|
|
|
func validateHourlyJSON(data []byte) error {
|
|
_, err := ValidateHourly(data)
|
|
return err
|
|
}
|