Bound generated text content and diagnostics
This commit is contained in:
112
internal/generatedtext/limits_test.go
Normal file
112
internal/generatedtext/limits_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user