Simplify and rationalize the hourly report template

This commit is contained in:
2026-06-14 12:54:32 -05:00
parent ceb00ad45e
commit 74e32eb18e
21 changed files with 363 additions and 211 deletions

View File

@@ -11,7 +11,7 @@ func TestTemplateLookup(t *testing.T) {
if err != nil {
t.Fatalf("Template() error = %v", err)
}
for _, want := range []string{"# {{ .Report.Title }}", "## Summary", "## Hourly Forecast", "## Weather Story"} {
for _, want := range []string{"# {{ .Report.Title }}", "**Updated:**", "## Current Conditions", "## Hourly Forecast", "## Forecast Discussion"} {
if !strings.Contains(source, want) {
t.Fatalf("template missing %q:\n%s", want, source)
}
@@ -38,10 +38,10 @@ func TestSchemaLookup(t *testing.T) {
if schema.AdditionalProperties {
t.Fatal("additionalProperties = true, want false")
}
if strings.Join(schema.Required, ",") != "summary,timing,impacts" {
t.Fatalf("required = %#v, want summary/timing/impacts", schema.Required)
if strings.Join(schema.Required, ",") != "summary,forecast_discussion" {
t.Fatalf("required = %#v, want summary/forecast_discussion", schema.Required)
}
for _, field := range []string{"summary", "timing", "impacts", "confidence"} {
for _, field := range []string{"summary", "forecast_discussion", "precipitation_timing", "confidence"} {
property, ok := schema.Properties[field].(map[string]any)
if !ok {
t.Fatalf("schema property %q missing or invalid", field)
@@ -58,34 +58,36 @@ func TestRenderHourly(t *testing.T) {
Title: "Hourly Report",
LocationName: "Brentwood",
ValidPeriodLabel: "May 29, 8:30 AM to 2:30 PM",
GeneratedAtLabel: "May 29, 8:30 AM",
GeneratedAtLabel: "Saturday, June 14, 2026 at 9:14 AM",
},
GeneratedText: testGeneratedText{
Summary: "Storm chances increase through late morning.",
Timing: "The main window is 10 AM to noon.",
Impacts: "Brief downpours may slow travel.",
Confidence: "Medium confidence in timing.",
Summary: "Storm chances increase through late morning.",
ForecastDiscussion: "A front will keep the region unsettled.",
PrecipitationTiming: "A cold front is moving into the region.",
Confidence: "Medium confidence in timing.",
},
Modules: testModules{
CurrentConditions: &testCurrentConditions{
ConditionText: "Partly cloudy",
ConditionTextLower: "partly cloudy",
TemperatureF: floatPtr(74),
ApparentTemperatureF: floatPtr(76),
RelativeHumidityPercent: floatPtr(71),
WindDirection: "S",
WindDirectionText: "south",
WindSpeedMph: floatPtr(8),
},
HourlyForecast: &testHourlyForecast{
Periods: []testHourlyPeriod{
{PeriodBegins: "9 AM", TextDescription: "Cloudy", TemperatureF: floatPtr(74), ProbabilityOfPrecipitationPercent: floatPtr(30), WindDirection: "S", WindSpeedMph: floatPtr(8)},
{PeriodBegins: "10 AM", TextDescription: "Showers", TemperatureF: floatPtr(75), ProbabilityOfPrecipitationPercent: floatPtr(70), WindDirection: "S", WindSpeedMph: floatPtr(10)},
{HourLabel: "9:00 AM", TextDescription: "Cloudy", TextDescriptionLower: "cloudy", TemperatureF: floatPtr(74), ProbabilityOfPrecipitationPercent: floatPtr(19), WindDirection: "S", WindSpeedMph: floatPtr(8)},
{HourLabel: "10:00 AM", TextDescription: "Showers", TextDescriptionLower: "showers", TemperatureF: floatPtr(75), ProbabilityOfPrecipitationPercent: floatPtr(70), MentionPrecipitation: true, WindDirection: "S", WindSpeedMph: floatPtr(10)},
},
},
PrecipTiming: &testPrecipTiming{
MaxPopPercent: intPtr(70),
MaxPopTime: "10 AM",
PrecipitationWindows: []testPrecipWindow{
{PeriodBegins: "10 AM", PeriodEnds: "12 PM", MaxPopPercent: intPtr(70), MaxPopTime: "10 AM"},
{PeriodBegins: "10 AM", PeriodBeginsHourLabel: "10:00 AM", PeriodEnds: "12 PM", PeriodEndsHourLabel: "12:00 PM", MaxPopPercent: intPtr(70), MaxPopTime: "10 AM", MaxPopHourLabel: "10:00 AM"},
},
},
AlertDigest: &testAlertDigest{
@@ -114,34 +116,71 @@ func TestRenderHourly(t *testing.T) {
text := string(rendered)
for _, want := range []string{
"# Hourly Report",
"Valid: May 29, 8:30 AM to 2:30 PM",
"**Updated:** Saturday, June 14, 2026 at 9:14 AM",
"Storm chances increase through late morning.",
"## Confidence",
"- 10 AM: Showers; 75 F; 70% precipitation; wind S 10 mph",
"- Flood Watch: Flood Watch until 2:30 PM (Moderate)",
"Short-term discussion favors increasing rain coverage.",
"Currently, it is 74°F and partly cloudy. It feels like 76°F, with a relative humidity of 71% and winds from the south at 8 mph.",
"## Active Alerts",
"- **Flood Watch**: Flood Watch until 2:30 PM",
"- **9:00 AM:** 74°F and cloudy.",
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
"- **10:00 AM** to **12:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 10:00 AM.",
"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, "19%") || strings.Contains(text, "wind S") || strings.Contains(text, "## Confidence") {
t.Fatalf("rendered template included omitted details:\n%s", text)
}
assertOrderedText(t, text, []string{
"# Hourly Report",
"## Summary",
"## Timing",
"## Impacts",
"## Confidence",
"## Current Conditions",
"## Active Alerts",
"## Hourly Forecast",
"## Precipitation Timing",
"## Alerts",
"## SPC Outlooks",
"## Forecast Discussion",
"## SPC Discussion",
"## Weather Story",
})
}
func TestRenderHourlyOmitsConditionalSectionsForClearWeather(t *testing.T) {
rendered, err := Render("hourly", testRenderContext{
Report: testReportContext{
Title: "Hourly Report",
GeneratedAtLabel: "Saturday, June 14, 2026 at 9:14 AM",
},
GeneratedText: testGeneratedText{
Summary: "Dry weather is expected through the next several hours.",
ForecastDiscussion: "Quiet conditions should persist through midday.",
},
Modules: testModules{
CurrentConditions: &testCurrentConditions{
ConditionTextLower: "cloudy",
TemperatureF: floatPtr(72),
},
HourlyForecast: &testHourlyForecast{
Periods: []testHourlyPeriod{
{HourLabel: "9:00 AM", TextDescriptionLower: "mostly cloudy", TemperatureF: floatPtr(71), ProbabilityOfPrecipitationPercent: floatPtr(10)},
},
},
AlertDigest: &testAlertDigest{},
PrecipTiming: &testPrecipTiming{
MaxPopPercent: intPtr(10),
},
},
})
if err != nil {
t.Fatalf("Render() error = %v", err)
}
text := string(rendered)
for _, unwanted := range []string{"## Active Alerts", "## Precipitation Timing", "Probability of precipitation is 10%", "wind"} {
if strings.Contains(text, unwanted) {
t.Fatalf("clear render includes %q:\n%s", unwanted, text)
}
}
}
func TestUnknownAssetsReturnActionableErrors(t *testing.T) {
if _, err := Template("daily"); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
t.Fatalf("Template() error = %v, want unknown template", err)
@@ -178,10 +217,10 @@ type testReportContext struct {
}
type testGeneratedText struct {
Summary string
Timing string
Impacts string
Confidence string
Summary string
ForecastDiscussion string
PrecipitationTiming string
Confidence string
}
type testModules struct {
@@ -197,11 +236,13 @@ type testModules struct {
type testCurrentConditions struct {
ConditionText string
ConditionTextLower string
TemperatureF *float64
ApparentTemperatureF *float64
RelativeHumidityPercent *float64
WindSpeedMph *float64
WindDirection string
WindDirectionText string
}
type testHourlyForecast struct {
@@ -209,14 +250,17 @@ type testHourlyForecast struct {
}
type testHourlyPeriod struct {
HourLabel string
PeriodBegins string
Name string
TextDescription string
TextDescriptionLower string
TemperatureF *float64
WindSpeedMph *float64
WindGustMph *float64
WindDirection string
ProbabilityOfPrecipitationPercent *float64
MentionPrecipitation bool
}
type testPrecipTiming struct {
@@ -227,10 +271,13 @@ type testPrecipTiming struct {
}
type testPrecipWindow struct {
PeriodBegins string
PeriodEnds string
MaxPopPercent *int
MaxPopTime string
PeriodBegins string
PeriodBeginsHourLabel string
PeriodEnds string
PeriodEndsHourLabel string
MaxPopPercent *int
MaxPopTime string
MaxPopHourLabel string
}
type testAlertDigest struct {