Consolidate generated text test ownership
This commit is contained in:
@@ -1,104 +0,0 @@
|
|||||||
package generatedtext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateDailyNormalizesFields(t *testing.T) {
|
|
||||||
value, err := ValidateDaily([]byte(`{
|
|
||||||
"summary": " Showers are possible during the selected day. ",
|
|
||||||
"forecast_discussion": [
|
|
||||||
" A front will keep rain chances in the forecast. ",
|
|
||||||
"",
|
|
||||||
" Temperatures stay seasonable by afternoon. "
|
|
||||||
],
|
|
||||||
"precipitation_timing": " Rain is most likely during the afternoon. "
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ValidateDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if value.Summary != "Showers are possible during the selected day." {
|
|
||||||
t.Fatalf("Summary = %q, want trimmed summary", value.Summary)
|
|
||||||
}
|
|
||||||
if strings.Join(value.ForecastDiscussion, "|") != "A front will keep rain chances in the forecast.|Temperatures stay seasonable by afternoon." {
|
|
||||||
t.Fatalf("ForecastDiscussion = %#v, want trimmed non-empty paragraphs", value.ForecastDiscussion)
|
|
||||||
}
|
|
||||||
if value.PrecipitationTiming != "Rain is most likely during the afternoon." {
|
|
||||||
t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDailyPreservesRequiredEmptyPrecipitationTiming(t *testing.T) {
|
|
||||||
value, err := ValidateDaily([]byte(`{
|
|
||||||
"summary": "Showers are possible during the selected day.",
|
|
||||||
"forecast_discussion": ["A front will keep rain chances in the forecast."],
|
|
||||||
"precipitation_timing": " "
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ValidateDaily() error = %v", err)
|
|
||||||
}
|
|
||||||
if value.PrecipitationTiming != "" {
|
|
||||||
t.Fatalf("PrecipitationTiming = %q, want required empty value", value.PrecipitationTiming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDailyRejectsInvalidInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "malformed",
|
|
||||||
in: `{`,
|
|
||||||
want: "decode daily generated text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown field",
|
|
||||||
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."],"extra":"value"}`,
|
|
||||||
want: "unsupported field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing summary",
|
|
||||||
in: `{"forecast_discussion":["A front will keep rain chances in the forecast."]}`,
|
|
||||||
want: "summary is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blank summary",
|
|
||||||
in: `{"summary":" ","forecast_discussion":["A front will keep rain chances in the forecast."]}`,
|
|
||||||
want: "summary is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing forecast discussion",
|
|
||||||
in: `{"summary":"Showers are possible during the selected day."}`,
|
|
||||||
want: "forecast discussion is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blank forecast discussion",
|
|
||||||
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":[" ",""]}`,
|
|
||||||
want: "forecast discussion is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forecast discussion wrong type",
|
|
||||||
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":"A front will keep rain chances in the forecast."}`,
|
|
||||||
want: "forecast discussion must be an array",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple values",
|
|
||||||
in: `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."]} {}`,
|
|
||||||
want: "multiple JSON values",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
_, err := ValidateDaily([]byte(test.in))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("ValidateDaily() error = nil, want error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), test.want) {
|
|
||||||
t.Fatalf("ValidateDaily() error = %v, want %q", err, test.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -119,6 +119,57 @@ func TestValidateDayStyleGeneratedTextSharedBehavior(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDayStyleValidatorsReturnReportSpecificTypes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
validate func([]byte) (any, error)
|
||||||
|
matches func(any) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "daily",
|
||||||
|
validate: func(data []byte) (any, error) {
|
||||||
|
return ValidateDaily(data)
|
||||||
|
},
|
||||||
|
matches: func(value any) bool {
|
||||||
|
_, ok := value.(Daily)
|
||||||
|
return ok
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "today",
|
||||||
|
validate: func(data []byte) (any, error) {
|
||||||
|
return ValidateToday(data)
|
||||||
|
},
|
||||||
|
matches: func(value any) bool {
|
||||||
|
_, ok := value.(Today)
|
||||||
|
return ok
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tomorrow",
|
||||||
|
validate: func(data []byte) (any, error) {
|
||||||
|
return ValidateTomorrow(data)
|
||||||
|
},
|
||||||
|
matches: func(value any) bool {
|
||||||
|
_, ok := value.(Tomorrow)
|
||||||
|
return ok
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
value, err := test.validate([]byte(`{"summary":"Summary.","forecast_discussion":["Discussion."],"precipitation_timing":""}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("validate() error = %v", err)
|
||||||
|
}
|
||||||
|
if !test.matches(value) {
|
||||||
|
t.Fatalf("validate() type = %T, want %s generated text", value, test.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func dayStyleFieldsForTest(t *testing.T, value any) dayStyleFields {
|
func dayStyleFieldsForTest(t *testing.T, value any) dayStyleFields {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -10,42 +10,6 @@ import (
|
|||||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
"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) {
|
func TestTypedValidatorsMatchEmbeddedSchemaObjectShape(t *testing.T) {
|
||||||
validators := []struct {
|
validators := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -68,6 +32,7 @@ func TestTypedValidatorsMatchEmbeddedSchemaObjectShape(t *testing.T) {
|
|||||||
{name: "missing", input: strings.Replace(validator.valid, `"summary":"Summary",`, "", 1)},
|
{name: "missing", input: strings.Replace(validator.valid, `"summary":"Summary",`, "", 1)},
|
||||||
{name: "additional", input: strings.Replace(validator.valid, "}", `,"extra":"value"}`, 1)},
|
{name: "additional", input: strings.Replace(validator.valid, "}", `,"extra":"value"}`, 1)},
|
||||||
{name: "case variant", input: strings.Replace(validator.valid, `"summary"`, `"Summary"`, 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: "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)},
|
{name: "wrong type", input: strings.Replace(validator.valid, `"summary":"Summary"`, `"summary":false`, 1)},
|
||||||
} {
|
} {
|
||||||
@@ -87,6 +52,27 @@ func TestTypedValidatorsMatchEmbeddedSchemaObjectShape(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestTypedValidatorsMatchEmbeddedSchemaContentLimits(t *testing.T) {
|
||||||
validators := []struct {
|
validators := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -71,27 +71,6 @@ func TestBuildHourlyRenderContext(t *testing.T) {
|
|||||||
if ctx.Modules.WeatherStory == nil || ctx.Modules.WeatherStory.Title != "Morning storms" {
|
if ctx.Modules.WeatherStory == nil || ctx.Modules.WeatherStory.Title != "Morning storms" {
|
||||||
t.Fatalf("Modules.WeatherStory = %#v, want story", ctx.Modules.WeatherStory)
|
t.Fatalf("Modules.WeatherStory = %#v, want story", ctx.Modules.WeatherStory)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("hourly", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
for _, want := range []string{
|
|
||||||
"# Hourly Report",
|
|
||||||
"**Updated:** Friday, May 29, 2026 at 8:30 AM",
|
|
||||||
"Storm chances increase through late morning.",
|
|
||||||
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
|
||||||
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM.",
|
|
||||||
"A cold front is moving into the region.",
|
|
||||||
"A front will keep the region unsettled.",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(text, "Avoid low-water crossings.") {
|
|
||||||
t.Fatalf("rendered template includes alert instruction:\n%s", text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||||
@@ -268,40 +247,6 @@ func TestBuildTodayRenderContext(t *testing.T) {
|
|||||||
if ctx.Modules.TodayPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
if ctx.Modules.TodayPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
||||||
t.Fatalf("Modules.TodayPlanning = %#v, want today planning facts", ctx.Modules.TodayPlanning)
|
t.Fatalf("Modules.TodayPlanning = %#v, want today planning facts", ctx.Modules.TodayPlanning)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("today", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
for _, want := range []string{
|
|
||||||
"# Today's Weather",
|
|
||||||
"**Forecast date:** Monday, June 15, 2026",
|
|
||||||
"Today starts quiet, then showers become more likely later in the day.",
|
|
||||||
"Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph.",
|
|
||||||
"- **Morning:** Partly cloudy, with temperatures in the low 60s.",
|
|
||||||
"- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.",
|
|
||||||
"## Precipitation Timing",
|
|
||||||
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.",
|
|
||||||
"The most likely rain window is from midafternoon into early evening.",
|
|
||||||
"Morning conditions should stay mostly dry.",
|
|
||||||
"Rain chances increase during the afternoon as deeper moisture arrives.",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertOrderedText(t, text, []string{
|
|
||||||
"# Today's Weather",
|
|
||||||
"## Current Conditions",
|
|
||||||
"## Daypart Forecast",
|
|
||||||
"- **Morning:**",
|
|
||||||
"- **Afternoon:**",
|
|
||||||
"## Precipitation Timing",
|
|
||||||
"## Forecast Discussion",
|
|
||||||
})
|
|
||||||
if strings.Contains(text, "## Planning Notes") || strings.Contains(text, "- Take sunglasses early.") || strings.Contains(text, "Forecast details are limited") {
|
|
||||||
t.Fatalf("rendered template included removed Today template content:\n%s", text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTodayRenderContextPreservesUnicodeDaypartDisplayNames(t *testing.T) {
|
func TestTodayRenderContextPreservesUnicodeDaypartDisplayNames(t *testing.T) {
|
||||||
@@ -324,12 +269,8 @@ func TestTodayRenderContextPreservesUnicodeDaypartDisplayNames(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("today", ctx)
|
if len(ctx.Modules.Dayparts) == 0 || ctx.Modules.Dayparts[0].Summary.DisplayName != "Mañana" {
|
||||||
if err != nil {
|
t.Fatalf("dayparts = %#v, want Unicode daypart display name", ctx.Modules.Dayparts)
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(rendered), "Mañana") {
|
|
||||||
t.Fatalf("rendered report = %q, want Unicode daypart display name", rendered)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,19 +292,6 @@ func TestBuildTodayRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|||||||
if ctx.Modules.HasDaypartDetails {
|
if ctx.Modules.HasDaypartDetails {
|
||||||
t.Fatal("Modules.HasDaypartDetails = true, want no displayable dayparts")
|
t.Fatal("Modules.HasDaypartDetails = true, want no displayable dayparts")
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("today", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
for _, unwanted := range []string{"## Current Conditions", "## Precipitation Timing", "## Planning Notes", "Forecast details are limited"} {
|
|
||||||
if strings.Contains(text, unwanted) {
|
|
||||||
t.Fatalf("rendered template included %q without module data:\n%s", unwanted, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !strings.Contains(text, "- No daypart forecast details are available.") {
|
|
||||||
t.Fatalf("rendered template missing daypart fallback:\n%s", text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *testing.T) {
|
func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *testing.T) {
|
||||||
@@ -593,42 +521,10 @@ func TestBuildDailyRenderContext(t *testing.T) {
|
|||||||
if ctx.Modules.DailyPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
if ctx.Modules.DailyPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
||||||
t.Fatalf("Modules.DailyPlanning = %#v, want daily planning facts", ctx.Modules.DailyPlanning)
|
t.Fatalf("Modules.DailyPlanning = %#v, want daily planning facts", ctx.Modules.DailyPlanning)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("daily", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
for _, want := range []string{
|
|
||||||
"# Monday's Weather",
|
|
||||||
"**Forecast date:** Monday, June 15, 2026",
|
|
||||||
"**Updated:** Saturday, June 13, 2026 at 9:14 AM",
|
|
||||||
"The selected day starts quiet, then showers become more likely later in the day.",
|
|
||||||
"- **Morning:** Partly cloudy, with temperatures in the low 60s.",
|
|
||||||
"- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.",
|
|
||||||
"- **Evening:** Mostly cloudy, with temperatures in the upper 60s.",
|
|
||||||
"## Precipitation Timing",
|
|
||||||
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.",
|
|
||||||
"The most likely rain window is from midafternoon into early evening.",
|
|
||||||
"Morning conditions should stay mostly dry.",
|
|
||||||
"Rain chances increase during the afternoon as deeper moisture arrives.",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertOrderedText(t, text, []string{
|
|
||||||
"# Monday's Weather",
|
|
||||||
"## Daypart Forecast",
|
|
||||||
"- **Morning:**",
|
|
||||||
"- **Afternoon:**",
|
|
||||||
"- **Evening:**",
|
|
||||||
"## Precipitation Timing",
|
|
||||||
"## Forecast Discussion",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatedGeneratedTextRendersAsPlainText(t *testing.T) {
|
func TestValidatedGeneratedTextBuildsAndRendersTypedContext(t *testing.T) {
|
||||||
generated, err := ValidateDaily([]byte("{\"summary\":\"Summary.\\n## Fabricated Alert\\n- Fabricated warning\",\"forecast_discussion\":[\"Discussion with [unsafe](javascript:alert(1))\"],\"precipitation_timing\":\"Timing.\\n```not code```\"}"))
|
generated, err := ValidateDaily([]byte(`{"summary":"Daily summary.","forecast_discussion":["Daily discussion."],"precipitation_timing":"Late-day showers."}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ValidateDaily() error = %v", err)
|
t.Fatalf("ValidateDaily() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -640,19 +536,9 @@ func TestValidatedGeneratedTextRendersAsPlainText(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Render() error = %v", err)
|
t.Fatalf("Render() error = %v", err)
|
||||||
}
|
}
|
||||||
text := string(rendered)
|
for _, want := range []string{"# Monday's Weather", "Daily summary.", "Daily discussion.", "Late-day showers."} {
|
||||||
for _, want := range []string{
|
if !strings.Contains(string(rendered), want) {
|
||||||
"Summary.\n\\## Fabricated Alert\n\\- Fabricated warning",
|
t.Fatalf("rendered report missing %q:\n%s", want, rendered)
|
||||||
"Discussion with \\[unsafe\\]\\(javascript:alert\\(1\\)\\)",
|
|
||||||
"Timing.\n\\`\\`\\`not code\\`\\`\\`",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("rendered report missing escaped generated prose %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, unwanted := range []string{"\n## Fabricated Alert", "\n- Fabricated warning", "[unsafe](javascript:alert(1))", "```not code```"} {
|
|
||||||
if strings.Contains(text, unwanted) {
|
|
||||||
t.Fatalf("rendered report preserved generated Markdown structure %q:\n%s", unwanted, text)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -672,17 +558,6 @@ func TestBuildDailyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|||||||
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || ctx.Modules.OutdoorWindows != nil || ctx.Modules.DailyPlanning != nil || len(ctx.Modules.Dayparts) != 0 {
|
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || ctx.Modules.OutdoorWindows != nil || ctx.Modules.DailyPlanning != nil || len(ctx.Modules.Dayparts) != 0 {
|
||||||
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("daily", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
if strings.Contains(text, "## Precipitation Timing") {
|
|
||||||
t.Fatalf("rendered template included precipitation section without windows:\n%s", text)
|
|
||||||
}
|
|
||||||
if !strings.Contains(text, "- No daypart forecast details are available.") {
|
|
||||||
t.Fatalf("rendered template missing daypart fallback:\n%s", text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildTomorrowRenderContext(t *testing.T) {
|
func TestBuildTomorrowRenderContext(t *testing.T) {
|
||||||
@@ -735,35 +610,6 @@ func TestBuildTomorrowRenderContext(t *testing.T) {
|
|||||||
if ctx.Modules.AlertDigest == nil || ctx.Modules.SPCConvectiveOutlooks == nil || ctx.Modules.AreaForecastDiscussion == nil || ctx.Modules.SPCConvectiveDiscussion == nil || ctx.Modules.WeatherStory == nil || ctx.Modules.TomorrowPlanning == nil {
|
if ctx.Modules.AlertDigest == nil || ctx.Modules.SPCConvectiveOutlooks == nil || ctx.Modules.AreaForecastDiscussion == nil || ctx.Modules.SPCConvectiveDiscussion == nil || ctx.Modules.WeatherStory == nil || ctx.Modules.TomorrowPlanning == nil {
|
||||||
t.Fatalf("optional modules missing from render context: %#v", ctx.Modules)
|
t.Fatalf("optional modules missing from render context: %#v", ctx.Modules)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("tomorrow", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
for _, want := range []string{
|
|
||||||
"# Monday's Weather",
|
|
||||||
"**Forecast date:** Monday, June 15, 2026",
|
|
||||||
"Tomorrow starts quiet, then showers become more likely later in the day.",
|
|
||||||
"- **Morning:** Partly cloudy, with temperatures in the low 60s.",
|
|
||||||
"- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.",
|
|
||||||
"## Precipitation Timing",
|
|
||||||
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM.",
|
|
||||||
"The most likely rain window is from midafternoon into early evening.",
|
|
||||||
"Morning conditions should stay mostly dry.",
|
|
||||||
"Rain chances increase during the afternoon as deeper moisture arrives.",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertOrderedText(t, text, []string{
|
|
||||||
"# Monday's Weather",
|
|
||||||
"## Daypart Forecast",
|
|
||||||
"- **Morning:**",
|
|
||||||
"- **Afternoon:**",
|
|
||||||
"## Precipitation Timing",
|
|
||||||
"## Forecast Discussion",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildTomorrowRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
func TestBuildTomorrowRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
||||||
@@ -781,17 +627,6 @@ func TestBuildTomorrowRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|||||||
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 {
|
if ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 {
|
||||||
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
||||||
}
|
}
|
||||||
rendered, err := reporttemplate.Render("tomorrow", ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Render() error = %v", err)
|
|
||||||
}
|
|
||||||
text := string(rendered)
|
|
||||||
if strings.Contains(text, "## Precipitation Timing") {
|
|
||||||
t.Fatalf("rendered template included precipitation section without windows:\n%s", text)
|
|
||||||
}
|
|
||||||
if !strings.Contains(text, "- No daypart forecast details are available.") {
|
|
||||||
t.Fatalf("rendered template missing daypart fallback:\n%s", text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMetadata() briefing.PreparedIdentity {
|
func testMetadata() briefing.PreparedIdentity {
|
||||||
@@ -1461,18 +1296,3 @@ func floatPtr(value float64) *float64 {
|
|||||||
func intPtr(value int) *int {
|
func intPtr(value int) *int {
|
||||||
return &value
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertOrderedText(t *testing.T, text string, ordered []string) {
|
|
||||||
t.Helper()
|
|
||||||
previousIndex := -1
|
|
||||||
for _, want := range ordered {
|
|
||||||
index := strings.Index(text, want)
|
|
||||||
if index < 0 {
|
|
||||||
t.Fatalf("text missing %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
if index <= previousIndex {
|
|
||||||
t.Fatalf("%q appears out of order in:\n%s", want, text)
|
|
||||||
}
|
|
||||||
previousIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package generatedtext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateTodayNormalizesFields(t *testing.T) {
|
|
||||||
value, err := ValidateToday([]byte(`{
|
|
||||||
"summary": " Showers are likely today. ",
|
|
||||||
"forecast_discussion": [
|
|
||||||
" A front will keep rain chances elevated. ",
|
|
||||||
"",
|
|
||||||
" Temperatures stay mild through the afternoon. "
|
|
||||||
],
|
|
||||||
"precipitation_timing": " Rain is most likely during the afternoon. "
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ValidateToday() error = %v", err)
|
|
||||||
}
|
|
||||||
if value.Summary != "Showers are likely today." {
|
|
||||||
t.Fatalf("Summary = %q, want trimmed summary", value.Summary)
|
|
||||||
}
|
|
||||||
if strings.Join(value.ForecastDiscussion, "|") != "A front will keep rain chances elevated.|Temperatures stay mild through the afternoon." {
|
|
||||||
t.Fatalf("ForecastDiscussion = %#v, want trimmed non-empty paragraphs", value.ForecastDiscussion)
|
|
||||||
}
|
|
||||||
if value.PrecipitationTiming != "Rain is most likely during the afternoon." {
|
|
||||||
t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateTodayPreservesRequiredEmptyPrecipitationTiming(t *testing.T) {
|
|
||||||
value, err := ValidateToday([]byte(`{
|
|
||||||
"summary": "Showers are likely today.",
|
|
||||||
"forecast_discussion": ["A front will keep rain chances elevated."],
|
|
||||||
"precipitation_timing": " "
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ValidateToday() error = %v", err)
|
|
||||||
}
|
|
||||||
if value.PrecipitationTiming != "" {
|
|
||||||
t.Fatalf("PrecipitationTiming = %q, want required empty value", value.PrecipitationTiming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateTodayRejectsInvalidInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "malformed",
|
|
||||||
in: `{`,
|
|
||||||
want: "decode today generated text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown field",
|
|
||||||
in: `{"summary":"Showers are likely today.","forecast_discussion":["A front will keep rain chances elevated."],"extra":"value"}`,
|
|
||||||
want: "unsupported field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing summary",
|
|
||||||
in: `{"forecast_discussion":["A front will keep rain chances elevated."]}`,
|
|
||||||
want: "summary is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blank summary",
|
|
||||||
in: `{"summary":" ","forecast_discussion":["A front will keep rain chances elevated."]}`,
|
|
||||||
want: "summary is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing forecast discussion",
|
|
||||||
in: `{"summary":"Showers are likely today."}`,
|
|
||||||
want: "forecast discussion is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blank forecast discussion",
|
|
||||||
in: `{"summary":"Showers are likely today.","forecast_discussion":[" ",""]}`,
|
|
||||||
want: "forecast discussion is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forecast discussion wrong type",
|
|
||||||
in: `{"summary":"Showers are likely today.","forecast_discussion":"A front will keep rain chances elevated."}`,
|
|
||||||
want: "forecast discussion must be an array",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple values",
|
|
||||||
in: `{"summary":"Showers are likely today.","forecast_discussion":["A front will keep rain chances elevated."]} {}`,
|
|
||||||
want: "multiple JSON values",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
_, err := ValidateToday([]byte(test.in))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("ValidateToday() error = nil, want error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), test.want) {
|
|
||||||
t.Fatalf("ValidateToday() error = %v, want %q", err, test.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package generatedtext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateTomorrowNormalizesFields(t *testing.T) {
|
|
||||||
value, err := ValidateTomorrow([]byte(`{
|
|
||||||
"summary": " Storms become more likely tomorrow. ",
|
|
||||||
"forecast_discussion": [
|
|
||||||
" A front will keep showers in the forecast. ",
|
|
||||||
"",
|
|
||||||
" Temperatures stay seasonable by afternoon. "
|
|
||||||
],
|
|
||||||
"precipitation_timing": " Rain is most likely before sunrise. "
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ValidateTomorrow() error = %v", err)
|
|
||||||
}
|
|
||||||
if value.Summary != "Storms become more likely tomorrow." {
|
|
||||||
t.Fatalf("Summary = %q, want trimmed summary", value.Summary)
|
|
||||||
}
|
|
||||||
if strings.Join(value.ForecastDiscussion, "|") != "A front will keep showers in the forecast.|Temperatures stay seasonable by afternoon." {
|
|
||||||
t.Fatalf("ForecastDiscussion = %#v, want trimmed non-empty paragraphs", value.ForecastDiscussion)
|
|
||||||
}
|
|
||||||
if value.PrecipitationTiming != "Rain is most likely before sunrise." {
|
|
||||||
t.Fatalf("PrecipitationTiming = %q, want trimmed precipitation timing", value.PrecipitationTiming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateTomorrowPreservesRequiredEmptyPrecipitationTiming(t *testing.T) {
|
|
||||||
value, err := ValidateTomorrow([]byte(`{
|
|
||||||
"summary": "Storms become more likely tomorrow.",
|
|
||||||
"forecast_discussion": ["A front will keep showers in the forecast."],
|
|
||||||
"precipitation_timing": " "
|
|
||||||
}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ValidateTomorrow() error = %v", err)
|
|
||||||
}
|
|
||||||
if value.PrecipitationTiming != "" {
|
|
||||||
t.Fatalf("PrecipitationTiming = %q, want required empty value", value.PrecipitationTiming)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateTomorrowRejectsInvalidInput(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
in string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "malformed",
|
|
||||||
in: `{`,
|
|
||||||
want: "decode tomorrow generated text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown field",
|
|
||||||
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."],"extra":"value"}`,
|
|
||||||
want: "unsupported field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing summary",
|
|
||||||
in: `{"forecast_discussion":["A front will keep showers in the forecast."]}`,
|
|
||||||
want: "summary is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blank summary",
|
|
||||||
in: `{"summary":" ","forecast_discussion":["A front will keep showers in the forecast."]}`,
|
|
||||||
want: "summary is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing forecast discussion",
|
|
||||||
in: `{"summary":"Storms become more likely tomorrow."}`,
|
|
||||||
want: "forecast discussion is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blank forecast discussion",
|
|
||||||
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":[" ",""]}`,
|
|
||||||
want: "forecast discussion is required",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "forecast discussion wrong type",
|
|
||||||
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":"A front will keep showers in the forecast."}`,
|
|
||||||
want: "forecast discussion must be an array",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple values",
|
|
||||||
in: `{"summary":"Storms become more likely tomorrow.","forecast_discussion":["A front will keep showers in the forecast."]} {}`,
|
|
||||||
want: "multiple JSON values",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
_, err := ValidateTomorrow([]byte(test.in))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("ValidateTomorrow() error = nil, want error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), test.want) {
|
|
||||||
t.Fatalf("ValidateTomorrow() error = %v, want %q", err, test.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -222,6 +222,7 @@ func TestSchemasAreCanonicalAndIndependent(t *testing.T) {
|
|||||||
if schema.Type != "object" || schema.AdditionalProperties || strings.Join(schema.Required, ",") != "summary,forecast_discussion,precipitation_timing" {
|
if schema.Type != "object" || schema.AdditionalProperties || strings.Join(schema.Required, ",") != "summary,forecast_discussion,precipitation_timing" {
|
||||||
t.Fatalf("schema = %#v, want strict generated-text object", schema)
|
t.Fatalf("schema = %#v, want strict generated-text object", schema)
|
||||||
}
|
}
|
||||||
|
assertGeneratedTextSchemaShape(t, id, schema.Properties)
|
||||||
if _, ok := schema.Properties["confidence"]; ok {
|
if _, ok := schema.Properties["confidence"]; ok {
|
||||||
t.Fatalf("schema properties = %#v, do not want retired confidence field", schema.Properties)
|
t.Fatalf("schema properties = %#v, do not want retired confidence field", schema.Properties)
|
||||||
}
|
}
|
||||||
@@ -237,6 +238,34 @@ func TestSchemasAreCanonicalAndIndependent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertGeneratedTextSchemaShape(t *testing.T, id string, properties map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
for _, name := range []string{"summary", "precipitation_timing"} {
|
||||||
|
property, ok := properties[name].(map[string]any)
|
||||||
|
if !ok || property["type"] != "string" {
|
||||||
|
t.Fatalf("schema property %q = %#v, want string", name, properties[name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discussion, ok := properties["forecast_discussion"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("schema forecast_discussion = %#v, want property", properties["forecast_discussion"])
|
||||||
|
}
|
||||||
|
if id == "hourly" {
|
||||||
|
if discussion["type"] != "string" || discussion["maxLength"] != float64(12_000) {
|
||||||
|
t.Fatalf("hourly forecast_discussion = %#v, want bounded string", discussion)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if discussion["type"] != "array" || discussion["minItems"] != float64(1) || discussion["maxItems"] != float64(12) {
|
||||||
|
t.Fatalf("%s forecast_discussion = %#v, want bounded non-empty array", id, discussion)
|
||||||
|
}
|
||||||
|
items, ok := discussion["items"].(map[string]any)
|
||||||
|
if !ok || items["type"] != "string" || items["maxLength"] != float64(4_000) {
|
||||||
|
t.Fatalf("%s forecast_discussion items = %#v, want bounded strings", id, discussion["items"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPromptkitInspectsEmbeddedPromptsOffline(t *testing.T) {
|
func TestPromptkitInspectsEmbeddedPromptsOffline(t *testing.T) {
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{},
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
promptkit.WithPromptFS(promptassets.PromptFS(), "."),
|
promptkit.WithPromptFS(promptassets.PromptFS(), "."),
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package reporttemplate
|
package reporttemplate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptassets"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTemplateLookup(t *testing.T) {
|
func TestTemplateLookup(t *testing.T) {
|
||||||
@@ -59,149 +56,6 @@ func TestTodayTemplateLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSchemaLookup(t *testing.T) {
|
|
||||||
data, err := promptassets.Schema("hourly")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Schema() error = %v", err)
|
|
||||||
}
|
|
||||||
assertStringSchema(t, data, "summary,forecast_discussion,precipitation_timing", []string{"summary", "forecast_discussion", "precipitation_timing"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTomorrowSchemaLookup(t *testing.T) {
|
|
||||||
data, err := promptassets.Schema("tomorrow")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Schema() error = %v", err)
|
|
||||||
}
|
|
||||||
schema := assertSchema(t, data, "summary,forecast_discussion,precipitation_timing")
|
|
||||||
property, ok := schema.Properties["forecast_discussion"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("schema property forecast_discussion missing or invalid")
|
|
||||||
}
|
|
||||||
if property["type"] != "array" {
|
|
||||||
t.Fatalf("forecast_discussion type = %v, want array", property["type"])
|
|
||||||
}
|
|
||||||
if property["minItems"] != float64(1) {
|
|
||||||
t.Fatalf("forecast_discussion minItems = %v, want 1", property["minItems"])
|
|
||||||
}
|
|
||||||
items, ok := property["items"].(map[string]any)
|
|
||||||
if !ok || items["type"] != "string" {
|
|
||||||
t.Fatalf("forecast_discussion items = %#v, want string items", property["items"])
|
|
||||||
}
|
|
||||||
for _, field := range []string{"summary", "precipitation_timing"} {
|
|
||||||
property, ok := schema.Properties[field].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("schema property %q missing or invalid", field)
|
|
||||||
}
|
|
||||||
if property["type"] != "string" {
|
|
||||||
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDailySchemaLookup(t *testing.T) {
|
|
||||||
data, err := promptassets.Schema("daily")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Schema() error = %v", err)
|
|
||||||
}
|
|
||||||
schema := assertSchema(t, data, "summary,forecast_discussion,precipitation_timing")
|
|
||||||
property, ok := schema.Properties["forecast_discussion"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("schema property forecast_discussion missing or invalid")
|
|
||||||
}
|
|
||||||
if property["type"] != "array" {
|
|
||||||
t.Fatalf("forecast_discussion type = %v, want array", property["type"])
|
|
||||||
}
|
|
||||||
if property["minItems"] != float64(1) {
|
|
||||||
t.Fatalf("forecast_discussion minItems = %v, want 1", property["minItems"])
|
|
||||||
}
|
|
||||||
items, ok := property["items"].(map[string]any)
|
|
||||||
if !ok || items["type"] != "string" {
|
|
||||||
t.Fatalf("forecast_discussion items = %#v, want string items", property["items"])
|
|
||||||
}
|
|
||||||
for _, field := range []string{"summary", "precipitation_timing"} {
|
|
||||||
property, ok := schema.Properties[field].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("schema property %q missing or invalid", field)
|
|
||||||
}
|
|
||||||
if property["type"] != "string" {
|
|
||||||
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTodaySchemaLookup(t *testing.T) {
|
|
||||||
data, err := promptassets.Schema("today")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Schema() error = %v", err)
|
|
||||||
}
|
|
||||||
schema := assertSchema(t, data, "summary,forecast_discussion,precipitation_timing")
|
|
||||||
property, ok := schema.Properties["forecast_discussion"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("schema property forecast_discussion missing or invalid")
|
|
||||||
}
|
|
||||||
if property["type"] != "array" {
|
|
||||||
t.Fatalf("forecast_discussion type = %v, want array", property["type"])
|
|
||||||
}
|
|
||||||
if property["minItems"] != float64(1) {
|
|
||||||
t.Fatalf("forecast_discussion minItems = %v, want 1", property["minItems"])
|
|
||||||
}
|
|
||||||
items, ok := property["items"].(map[string]any)
|
|
||||||
if !ok || items["type"] != "string" {
|
|
||||||
t.Fatalf("forecast_discussion items = %#v, want string items", property["items"])
|
|
||||||
}
|
|
||||||
for _, field := range []string{"summary", "precipitation_timing"} {
|
|
||||||
property, ok := schema.Properties[field].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("schema property %q missing or invalid", field)
|
|
||||||
}
|
|
||||||
if property["type"] != "string" {
|
|
||||||
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertStringSchema(t *testing.T, data []byte, required string, fields []string) {
|
|
||||||
t.Helper()
|
|
||||||
schema := assertSchema(t, data, required)
|
|
||||||
for _, field := range fields {
|
|
||||||
property, ok := schema.Properties[field].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("schema property %q missing or invalid", field)
|
|
||||||
}
|
|
||||||
if property["type"] != "string" {
|
|
||||||
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertSchema(t *testing.T, data []byte, required string) struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
AdditionalProperties bool `json:"additionalProperties"`
|
|
||||||
Required []string `json:"required"`
|
|
||||||
Properties map[string]any `json:"properties"`
|
|
||||||
} {
|
|
||||||
t.Helper()
|
|
||||||
var schema struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
AdditionalProperties bool `json:"additionalProperties"`
|
|
||||||
Required []string `json:"required"`
|
|
||||||
Properties map[string]any `json:"properties"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &schema); err != nil {
|
|
||||||
t.Fatalf("schema is invalid JSON: %v", err)
|
|
||||||
}
|
|
||||||
if schema.Type != "object" {
|
|
||||||
t.Fatalf("schema type = %q, want object", schema.Type)
|
|
||||||
}
|
|
||||||
if schema.AdditionalProperties {
|
|
||||||
t.Fatal("additionalProperties = true, want false")
|
|
||||||
}
|
|
||||||
if strings.Join(schema.Required, ",") != required {
|
|
||||||
t.Fatalf("required = %#v, want %s", schema.Required, required)
|
|
||||||
}
|
|
||||||
return schema
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderHourly(t *testing.T) {
|
func TestRenderHourly(t *testing.T) {
|
||||||
rendered, err := Render("hourly", testRenderContext{
|
rendered, err := Render("hourly", testRenderContext{
|
||||||
Report: testReportContext{
|
Report: testReportContext{
|
||||||
|
|||||||
Reference in New Issue
Block a user