package generatedtext import ( "bytes" "fmt" "strings" "testing" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptassets" "github.com/santhosh-tekuri/jsonschema/v6" ) 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: "discussion case variant", input: strings.Replace(validator.valid, `"forecast_discussion"`, `"Forecast_Discussion"`, 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 TestTypedValidatorsRejectDuplicateFields(t *testing.T) { validators := []struct { name string input string validate func([]byte) error }{ {name: "daily", input: `{"summary":"Summary","summary":"Other","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateDailyJSON}, {name: "today", input: `{"summary":"Summary","summary":"Other","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTodayJSON}, {name: "tomorrow", input: `{"summary":"Summary","summary":"Other","forecast_discussion":["Discussion"],"precipitation_timing":""}`, validate: validateTomorrowJSON}, {name: "hourly", input: `{"summary":"Summary","summary":"Other","forecast_discussion":"Discussion","precipitation_timing":""}`, validate: validateHourlyJSON}, } for _, validator := range validators { t.Run(validator.name, func(t *testing.T) { if err := validator.validate([]byte(validator.input)); err == nil { t.Fatal("validate() error = nil, want duplicate-field rejection") } }) } } 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 }