package generatedtext import ( "fmt" "strings" "testing" ) func TestValidateHourlyGeneratedTextEnforcesContentLimits(t *testing.T) { input := func(summary, discussion, timing string) []byte { return []byte(fmt.Sprintf(`{"summary":%q,"forecast_discussion":%q,"precipitation_timing":%q}`, summary, discussion, timing)) } for _, test := range []struct { name string input []byte want string }{ {name: "summary at limit", input: input(strings.Repeat("s", maxGeneratedTextStringCharacters), "discussion", "")}, {name: "summary over limit", input: input(strings.Repeat("s", maxGeneratedTextStringCharacters+1), "discussion", ""), want: "summary exceeds"}, {name: "discussion at limit", input: input("summary", strings.Repeat("d", maxHourlyDiscussionCharacters), "")}, {name: "discussion over limit", input: input("summary", strings.Repeat("d", maxHourlyDiscussionCharacters+1), ""), want: "forecast discussion exceeds"}, {name: "total at limit", input: input(strings.Repeat("s", 4_000), strings.Repeat("d", 12_000), strings.Repeat("t", 4_000))}, } { t.Run(test.name, func(t *testing.T) { err := validateHourlyJSON(test.input) if test.want == "" && err != nil { t.Fatalf("validate() error = %v", err) } if test.want != "" && (err == nil || !strings.Contains(err.Error(), test.want)) { t.Fatalf("validate() error = %v, want %q", err, test.want) } }) } } func TestValidateDayStyleGeneratedTextEnforcesDiscussionLimits(t *testing.T) { for _, test := range []struct { name string input string want string }{ { name: "paragraph count at limit", input: fmt.Sprintf(`{"summary":"summary","forecast_discussion":[%s],"precipitation_timing":""}`, strings.TrimSuffix(strings.Repeat(`"p",`, maxDayStyleDiscussionParagraphs), ",")), }, { name: "paragraph count over limit", input: fmt.Sprintf(`{"summary":"summary","forecast_discussion":[%s],"precipitation_timing":""}`, strings.TrimSuffix(strings.Repeat(`"p",`, maxDayStyleDiscussionParagraphs+1), ",")), want: "12-paragraph limit", }, { name: "paragraph at limit", input: fmt.Sprintf(`{"summary":"summary","forecast_discussion":[%q],"precipitation_timing":""}`, strings.Repeat("p", maxDayStyleDiscussionParagraphChars)), }, { name: "paragraph over limit", input: fmt.Sprintf(`{"summary":"summary","forecast_discussion":[%q],"precipitation_timing":""}`, strings.Repeat("p", maxDayStyleDiscussionParagraphChars+1)), want: "paragraphs exceed the 4000-character limit", }, { name: "total at limit", input: fmt.Sprintf(`{"summary":%q,"forecast_discussion":[%q,%q,%q,%q],"precipitation_timing":""}`, strings.Repeat("s", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000)), }, { name: "total over limit", input: fmt.Sprintf(`{"summary":%q,"forecast_discussion":[%q,%q,%q,%q,%q],"precipitation_timing":""}`, strings.Repeat("s", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000), strings.Repeat("p", 4_000)), want: "exceeds the 20000-character limit", }, } { t.Run(test.name, func(t *testing.T) { err := validateDailyJSON([]byte(test.input)) if test.want == "" && err != nil { t.Fatalf("validate() error = %v", err) } if test.want != "" && (err == nil || !strings.Contains(err.Error(), test.want)) { t.Fatalf("validate() error = %v, want %q", err, test.want) } }) } } func TestValidateGeneratedTextRejectsOversizedRawOutputWithoutLeakingContent(t *testing.T) { valid := []byte(`{"summary":"summary","forecast_discussion":"discussion","precipitation_timing":""}`) atLimit := append([]byte(strings.Repeat(" ", MaxGeneratedTextBytes-len(valid))), valid...) if err := validateHourlyJSON(atLimit); err != nil { t.Fatalf("validate() at raw limit error = %v", err) } marker := "provider-controlled-marker" unknownPrefix := `{"summary":"summary","forecast_discussion":"discussion","precipitation_timing":"","` unknownSuffix := `":"value"}` unknownKey := marker + strings.Repeat("x", MaxGeneratedTextBytes-len(unknownPrefix)-len(unknownSuffix)-len(marker)) unknownField := []byte(unknownPrefix + unknownKey + unknownSuffix) err := validateHourlyJSON(unknownField) if err == nil || !strings.Contains(err.Error(), "unsupported field") { t.Fatalf("validate() error = %v, want unsupported field error", err) } if len(err.Error()) > 128 || strings.Contains(err.Error(), marker) { t.Fatalf("validate() leaked unknown field content: %q", err) } input := append(atLimit, []byte(marker)...) err = validateHourlyJSON(input) if err == nil || !strings.Contains(err.Error(), "65536-byte limit") { t.Fatalf("validate() error = %v, want raw size error", err) } if len(err.Error()) > 128 || strings.Contains(err.Error(), marker) { t.Fatalf("validate() leaked oversized provider content: %q", err) } }