2026-06-14 04:46:53 +00:00
package generatedtext
import (
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
2026-06-14 08:57:53 -05:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
2026-06-14 04:46:53 +00:00
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/reporttemplate"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
)
func TestBuildHourlyRenderContext ( t * testing . T ) {
metadata := testMetadata ( )
snapshot := testSnapshot ( t )
generated := Hourly {
2026-06-14 12:54:32 -05:00
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." ,
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
collected := testCollected ( )
derived := testDerived ( )
ctx , err := BuildHourlyRenderContext ( metadata , snapshot , generated , collected , derived )
2026-06-14 04:46:53 +00:00
if err != nil {
t . Fatalf ( "BuildHourlyRenderContext() error = %v" , err )
}
2026-06-14 08:57:53 -05:00
if ctx . Report . Title != "Hourly Report" {
t . Fatalf ( "Report.Title = %q, want Hourly Report" , ctx . Report . Title )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Report . LocationName != "Brentwood, MO" {
t . Fatalf ( "Report.LocationName = %q, want Brentwood, MO" , ctx . Report . LocationName )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Report . ValidPeriodLabel != "2026-05-29 at 8:30 AM to 2026-05-29 at 2:30 PM" {
t . Fatalf ( "Report.ValidPeriodLabel = %q, want friendly period" , ctx . Report . ValidPeriodLabel )
2026-06-14 04:46:53 +00:00
}
2026-06-14 12:54:32 -05:00
if ctx . Report . GeneratedAtLabel != "Friday, May 29, 2026 at 8:30 AM" {
t . Fatalf ( "Report.GeneratedAtLabel = %q, want friendly generated-at label" , ctx . Report . GeneratedAtLabel )
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . CurrentConditions == nil || ctx . Modules . CurrentConditions . ConditionText != "Partly cloudy" || ctx . Modules . CurrentConditions . TemperatureF == nil || * ctx . Modules . CurrentConditions . TemperatureF != 74 {
t . Fatalf ( "Modules.CurrentConditions = %#v, want structured current conditions" , ctx . Modules . CurrentConditions )
2026-06-14 04:46:53 +00:00
}
2026-06-15 20:32:12 +00:00
if ctx . Modules . CurrentConditions . ConditionTextLower != "partly cloudy" || ctx . Modules . CurrentConditions . WindDirectionText != "south" {
t . Fatalf ( "Modules.CurrentConditions helpers = %#v, want rich template helper fields" , ctx . Modules . CurrentConditions )
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . HourlyForecast == nil || len ( ctx . Modules . HourlyForecast . Periods ) != 2 {
t . Fatalf ( "Modules.HourlyForecast = %#v, want 2 periods" , ctx . Modules . HourlyForecast )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if period := ctx . Modules . HourlyForecast . Periods [ 1 ] ; period . PeriodBegins != "2026-05-29 at 10:00 AM" || period . TextDescription != "Showers" || period . ProbabilityOfPrecipitationPercent == nil || * period . ProbabilityOfPrecipitationPercent != 70 {
t . Fatalf ( "Modules.HourlyForecast.Periods[1] = %#v, want 10 AM showers row" , period )
2026-06-14 04:46:53 +00:00
}
2026-06-15 20:32:12 +00:00
if period := ctx . Modules . HourlyForecast . Periods [ 1 ] ; period . HourLabel != "10:00 AM" || period . TextDescriptionLower != "showers" || ! period . MentionPrecipitation {
t . Fatalf ( "Modules.HourlyForecast.Periods[1] helpers = %#v, want rich template helper fields" , period )
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . PrecipTiming == nil || ctx . Modules . PrecipTiming . MaxPopPercent == nil || * ctx . Modules . PrecipTiming . MaxPopPercent != 70 {
t . Fatalf ( "Modules.PrecipTiming = %#v, want max pop" , ctx . Modules . PrecipTiming )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . AlertDigest == nil || len ( ctx . Modules . AlertDigest . Relevant ) != 1 || ctx . Modules . AlertDigest . Relevant [ 0 ] . Event != "Flood Watch" {
t . Fatalf ( "Modules.AlertDigest = %#v, want alert" , ctx . Modules . AlertDigest )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . SPCConvectiveOutlooks == nil || len ( ctx . Modules . SPCConvectiveOutlooks . Outlooks ) != 1 || ctx . Modules . SPCConvectiveOutlooks . Outlooks [ 0 ] . LabelText != "Slight Risk" {
t . Fatalf ( "Modules.SPCConvectiveOutlooks = %#v, want outlook" , ctx . Modules . SPCConvectiveOutlooks )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . AreaForecastDiscussion == nil || ctx . Modules . AreaForecastDiscussion . ShortTerm != "Short-term discussion favors increasing rain coverage." {
t . Fatalf ( "Modules.AreaForecastDiscussion = %#v, want discussion text" , ctx . Modules . AreaForecastDiscussion )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . SPCConvectiveDiscussion == nil || len ( ctx . Modules . SPCConvectiveDiscussion . Discussions ) != 1 || ctx . Modules . SPCConvectiveDiscussion . Discussions [ 0 ] . Headline != "Mesoscale discussion" {
t . Fatalf ( "Modules.SPCConvectiveDiscussion = %#v, want discussion" , ctx . Modules . SPCConvectiveDiscussion )
2026-06-14 04:46:53 +00:00
}
2026-06-14 08:57:53 -05:00
if ctx . Modules . WeatherStory == nil || ctx . Modules . WeatherStory . Title != "Morning storms" {
t . Fatalf ( "Modules.WeatherStory = %#v, want story" , ctx . Modules . WeatherStory )
}
if ! ctx . Collected . FetchedAt . Equal ( collected . FetchedAt ) {
t . Fatalf ( "Collected.FetchedAt = %s, want %s" , ctx . Collected . FetchedAt , collected . FetchedAt )
}
if ! ctx . Derived . PrecipTiming . ThunderMentioned {
t . Fatalf ( "Derived.PrecipTiming.ThunderMentioned = false, want true" )
2026-06-14 04:46:53 +00:00
}
rendered , err := reporttemplate . Render ( "hourly" , ctx )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"# Hourly Report" ,
2026-06-14 12:54:32 -05:00
"**Updated:** Friday, May 29, 2026 at 8:30 AM" ,
2026-06-14 04:46:53 +00:00
"Storm chances increase through late morning." ,
2026-06-14 12:54:32 -05:00
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%." ,
"- **Flood Watch**: Flood Watch until early afternoon" ,
"A cold front is moving into the region." ,
"A front will keep the region unsettled." ,
2026-06-14 04:46:53 +00:00
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
}
2026-06-14 08:57:53 -05:00
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules ( t * testing . T ) {
2026-06-14 04:46:53 +00:00
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{ ID : module . CurrentConditions , StanzaName : string ( module . CurrentConditions ) , Value : briefing . CurrentConditionsModule { } } ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
2026-06-14 08:57:53 -05:00
ctx , err := BuildHourlyRenderContext ( testMetadata ( ) , snapshot , Hourly {
2026-06-14 12:54:32 -05:00
Summary : "Storm chances increase." ,
ForecastDiscussion : "A front will keep the region unsettled." ,
2026-06-14 08:57:53 -05:00
} , testCollected ( ) , facts . DerivedFacts { } )
if err != nil {
t . Fatalf ( "BuildHourlyRenderContext() error = %v" , err )
2026-06-14 04:46:53 +00:00
}
2026-06-15 12:41:45 +00:00
if ctx . Modules . CurrentConditions == nil {
t . Fatal ( "Modules.CurrentConditions = nil, want populated module" )
}
if ctx . Modules . HourlyForecast != nil || ctx . Modules . PrecipTiming != nil || ctx . Modules . AlertDigest != nil {
t . Fatalf ( "Modules = %#v, want omitted optional modules to remain nil" , ctx . Modules )
}
}
func TestBuildRenderContextReportsModuleExtractionError ( t * testing . T ) {
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{ ID : module . CurrentConditions , StanzaName : string ( module . CurrentConditions ) , Value : "not a current conditions stanza" } ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
_ , err = BuildHourlyRenderContext ( testMetadata ( ) , snapshot , Hourly {
Summary : "Storm chances increase." ,
ForecastDiscussion : "A front will keep the region unsettled." ,
} , testCollected ( ) , facts . DerivedFacts { } )
if err == nil {
t . Fatal ( "BuildHourlyRenderContext() error = nil, want extraction error" )
}
if ! strings . Contains ( err . Error ( ) , string ( module . CurrentConditions ) ) {
t . Fatalf ( "BuildHourlyRenderContext() error = %v, want module ID" , err )
2026-06-14 04:46:53 +00:00
}
}
2026-06-15 14:45:49 +00:00
func TestBuildTodayRenderContext ( t * testing . T ) {
metadata := testTodayMetadata ( )
snapshot := testTodaySnapshot ( t )
generated := Today {
Summary : "Today starts quiet, then showers become more likely later in the day." ,
ForecastDiscussion : [ ] string {
"Morning conditions should stay mostly dry." ,
"Rain chances increase during the afternoon as deeper moisture arrives." ,
} ,
PrecipitationTiming : "The most likely rain window is from midafternoon into early evening." ,
}
collected := testCollected ( )
derived := testTodayDerived ( )
ctx , err := BuildTodayRenderContext ( metadata , snapshot , generated , collected , derived )
if err != nil {
t . Fatalf ( "BuildTodayRenderContext() error = %v" , err )
}
if ctx . Report . Title != "Today's Weather" {
t . Fatalf ( "Report.Title = %q, want Today's Weather" , ctx . Report . Title )
}
if ctx . Report . ForecastDateLabel != "Monday, June 15, 2026" || ctx . Report . ForecastDayName != "Monday" {
t . Fatalf ( "forecast date labels = %q/%q, want Monday labels" , ctx . Report . ForecastDateLabel , ctx . Report . ForecastDayName )
}
if ctx . Report . GeneratedAtLabel != "Monday, June 15, 2026 at 7:14 AM" {
t . Fatalf ( "Report.GeneratedAtLabel = %q, want friendly generated-at label" , ctx . Report . GeneratedAtLabel )
}
if ctx . Modules . Metadata == nil || ctx . Modules . Metadata . ReportID != report . Today {
t . Fatalf ( "Modules.Metadata = %#v, want today metadata" , ctx . Modules . Metadata )
}
if ctx . Modules . CurrentConditions == nil || ctx . Modules . HourlyForecast == nil {
t . Fatalf ( "current/hourly modules = %#v/%#v, want available in render context" , ctx . Modules . CurrentConditions , ctx . Modules . HourlyForecast )
}
if ctx . Modules . DerivedDailySummary == nil || ctx . Modules . DerivedDailySummary . HighTempF == nil || * ctx . Modules . DerivedDailySummary . HighTempF != 74 {
t . Fatalf ( "Modules.DerivedDailySummary = %#v, want daily summary" , ctx . Modules . DerivedDailySummary )
}
if ctx . Modules . DerivedDaypartSummaries == nil || len ( * ctx . Modules . DerivedDaypartSummaries ) != 2 {
t . Fatalf ( "Modules.DerivedDaypartSummaries = %#v, want daypart map" , ctx . Modules . DerivedDaypartSummaries )
}
if len ( ctx . Modules . Dayparts ) != 2 || ctx . Modules . Dayparts [ 0 ] . Key != "morning" || ctx . Modules . Dayparts [ 1 ] . Key != "afternoon" {
t . Fatalf ( "Modules.Dayparts = %#v, want configured order" , ctx . Modules . Dayparts )
}
2026-06-15 20:38:56 +00:00
if morning := ( * ctx . Modules . DerivedDaypartSummaries ) [ "morning" ] ; morning . DominantConditionDisplay == "" || morning . DominantConditionLower == "" || morning . TemperatureSteadyPhraseF == "" {
t . Fatalf ( "today rich morning daypart helpers = %#v, want render-context helper fields" , morning )
}
if afternoon := ( * ctx . Modules . DerivedDaypartSummaries ) [ "afternoon" ] ; afternoon . MaxPopTimeLabel != "3:00 PM" {
t . Fatalf ( "today rich afternoon daypart = %#v, want max pop time label helper" , afternoon )
}
2026-06-15 14:45:49 +00:00
if ctx . Modules . PrecipTiming == nil || len ( ctx . Modules . PrecipTiming . PrecipitationWindows ) != 1 {
t . Fatalf ( "Modules.PrecipTiming = %#v, want precipitation window" , ctx . Modules . PrecipTiming )
}
if ctx . Modules . AlertDigest == nil || ctx . Modules . SPCConvectiveOutlooks == nil || ctx . Modules . AreaForecastDiscussion == nil || ctx . Modules . SPCConvectiveDiscussion == nil || ctx . Modules . WeatherStory == nil || ctx . Modules . TodayPlanning == nil {
t . Fatalf ( "optional modules missing from render context: %#v" , ctx . Modules )
}
if ctx . Modules . TodayPlanning . MorningReadiness [ 0 ] != "Take sunglasses early." {
t . Fatalf ( "Modules.TodayPlanning = %#v, want today planning facts" , ctx . Modules . TodayPlanning )
}
if ! ctx . Collected . FetchedAt . Equal ( collected . FetchedAt ) {
t . Fatalf ( "Collected.FetchedAt = %s, want %s" , ctx . Collected . FetchedAt , collected . FetchedAt )
}
if len ( ctx . Derived . DaypartSummaries ) != 2 {
t . Fatalf ( "Derived.DaypartSummaries = %#v, want passthrough facts" , ctx . Derived . DaypartSummaries )
}
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." ,
2026-06-15 11:01:03 -05:00
"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." ,
2026-06-15 14:45:49 +00:00
"- **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**: Precipitation is expected during this period. 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" ,
} )
2026-06-15 11:01:03 -05:00
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 )
}
2026-06-15 14:45:49 +00:00
}
func TestBuildTodayRenderContextAllowsOmittedOptionalModules ( t * testing . T ) {
snapshot , err := module . NewSnapshot ( nil )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
ctx , err := BuildTodayRenderContext ( testTodayMetadata ( ) , snapshot , Today {
Summary : "Dry weather is expected today." ,
ForecastDiscussion : [ ] string { "High pressure keeps conditions quiet." } ,
} , testCollected ( ) , facts . DerivedFacts { } )
if err != nil {
t . Fatalf ( "BuildTodayRenderContext() error = %v" , err )
}
if ctx . Modules . Metadata != nil || ctx . Modules . CurrentConditions != nil || ctx . Modules . PrecipTiming != nil || len ( ctx . Modules . Dayparts ) != 0 || ctx . Modules . TodayPlanning != nil {
t . Fatalf ( "Modules = %#v, want omitted optional modules" , ctx . Modules )
}
rendered , err := reporttemplate . Render ( "today" , ctx )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
2026-06-15 11:01:03 -05:00
for _ , unwanted := range [ ] string { "## Current Conditions" , "## Precipitation Timing" , "## Planning Notes" , "Forecast details are limited" } {
2026-06-15 14:45:49 +00:00
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 )
}
}
2026-06-15 16:29:18 +00:00
func TestBuildDailyRenderContext ( t * testing . T ) {
metadata := testDailyMetadata ( )
snapshot := testDailySnapshot ( t )
generated := Daily {
Summary : "The selected day starts quiet, then showers become more likely later in the day." ,
ForecastDiscussion : [ ] string {
"Morning conditions should stay mostly dry." ,
"Rain chances increase during the afternoon as deeper moisture arrives." ,
} ,
PrecipitationTiming : "The most likely rain window is from midafternoon into early evening." ,
}
collected := testCollected ( )
derived := testDailyDerived ( )
ctx , err := BuildDailyRenderContext ( metadata , snapshot , generated , collected , derived )
if err != nil {
t . Fatalf ( "BuildDailyRenderContext() error = %v" , err )
}
if ctx . Report . Title != "Monday's Weather" {
t . Fatalf ( "Report.Title = %q, want Monday's Weather" , ctx . Report . Title )
}
if ctx . Report . ForecastDateLabel != "Monday, June 15, 2026" || ctx . Report . ForecastDayName != "Monday" {
t . Fatalf ( "forecast date labels = %q/%q, want Monday labels" , ctx . Report . ForecastDateLabel , ctx . Report . ForecastDayName )
}
if ctx . Report . GeneratedAtLabel != "Saturday, June 13, 2026 at 9:14 AM" {
t . Fatalf ( "Report.GeneratedAtLabel = %q, want friendly generated-at label" , ctx . Report . GeneratedAtLabel )
}
if ! ctx . Report . ValidPeriod . Start . Equal ( metadata . ValidPeriod . Start ) || ! ctx . Report . ValidPeriod . End . Equal ( metadata . ValidPeriod . End ) || ctx . Report . Timezone != "America/Chicago" {
t . Fatalf ( "period/timezone = %#v/%q, want metadata passthrough" , ctx . Report . ValidPeriod , ctx . Report . Timezone )
}
2026-06-15 16:43:06 +00:00
if ctx . Modules . Metadata == nil || ctx . Modules . Metadata . ReportID != report . Daily {
2026-06-15 16:29:18 +00:00
t . Fatalf ( "Modules.Metadata = %#v, want daily metadata" , ctx . Modules . Metadata )
}
if ctx . Modules . CurrentConditions == nil || ctx . Modules . HourlyForecast == nil {
t . Fatalf ( "current/hourly modules = %#v/%#v, want available in render context" , ctx . Modules . CurrentConditions , ctx . Modules . HourlyForecast )
}
if ctx . Modules . DerivedDailySummary == nil || ctx . Modules . DerivedDailySummary . HighTempF == nil || * ctx . Modules . DerivedDailySummary . HighTempF != 74 {
t . Fatalf ( "Modules.DerivedDailySummary = %#v, want daily summary" , ctx . Modules . DerivedDailySummary )
}
if ctx . Modules . DerivedDaypartSummaries == nil || len ( * ctx . Modules . DerivedDaypartSummaries ) != 3 {
t . Fatalf ( "Modules.DerivedDaypartSummaries = %#v, want daypart map" , ctx . Modules . DerivedDaypartSummaries )
}
if len ( ctx . Modules . Dayparts ) != 3 || ctx . Modules . Dayparts [ 0 ] . Key != "morning" || ctx . Modules . Dayparts [ 1 ] . Key != "afternoon" || ctx . Modules . Dayparts [ 2 ] . Key != "evening" {
t . Fatalf ( "Modules.Dayparts = %#v, want derived order followed by remaining keys" , ctx . Modules . Dayparts )
}
2026-06-15 20:38:56 +00:00
if morning := ( * ctx . Modules . DerivedDaypartSummaries ) [ "morning" ] ; morning . DominantConditionDisplay == "" || morning . DominantConditionLower == "" || morning . TemperatureSteadyPhraseF == "" {
t . Fatalf ( "daily rich morning daypart helpers = %#v, want render-context helper fields" , morning )
}
if afternoon := ( * ctx . Modules . DerivedDaypartSummaries ) [ "afternoon" ] ; afternoon . MaxPopTimeLabel != "3:00 PM" {
t . Fatalf ( "daily rich afternoon daypart = %#v, want max pop time label helper" , afternoon )
}
2026-06-15 16:29:18 +00:00
if ctx . Modules . PrecipTiming == nil || len ( ctx . Modules . PrecipTiming . PrecipitationWindows ) != 1 {
t . Fatalf ( "Modules.PrecipTiming = %#v, want precipitation window" , ctx . Modules . PrecipTiming )
}
if ctx . Modules . AlertDigest == nil || ctx . Modules . SPCConvectiveOutlooks == nil || ctx . Modules . AreaForecastDiscussion == nil || ctx . Modules . SPCConvectiveDiscussion == nil || ctx . Modules . WeatherStory == nil || ctx . Modules . OutdoorWindows == nil || ctx . Modules . DailyPlanning == nil {
t . Fatalf ( "optional modules missing from render context: %#v" , ctx . Modules )
}
if ctx . Modules . OutdoorWindows . Best == nil || ctx . Modules . OutdoorWindows . Best . Daypart != "morning" {
t . Fatalf ( "Modules.OutdoorWindows = %#v, want morning best window" , ctx . Modules . OutdoorWindows )
}
if ctx . Modules . DailyPlanning . MorningReadiness [ 0 ] != "Take sunglasses early." {
t . Fatalf ( "Modules.DailyPlanning = %#v, want daily planning facts" , ctx . Modules . DailyPlanning )
}
if ! ctx . Collected . FetchedAt . Equal ( collected . FetchedAt ) {
t . Fatalf ( "Collected.FetchedAt = %s, want %s" , ctx . Collected . FetchedAt , collected . FetchedAt )
}
if len ( ctx . Derived . DaypartSummaries ) != 2 {
t . Fatalf ( "Derived.DaypartSummaries = %#v, want passthrough facts" , ctx . Derived . DaypartSummaries )
}
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**: Precipitation is expected during this period. 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 TestBuildDailyRenderContextAllowsOmittedOptionalModules ( t * testing . T ) {
snapshot , err := module . NewSnapshot ( nil )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
ctx , err := BuildDailyRenderContext ( testDailyMetadata ( ) , snapshot , Daily {
Summary : "Dry weather is expected for the selected day." ,
ForecastDiscussion : [ ] string { "High pressure keeps conditions quiet." } ,
} , testCollected ( ) , facts . DerivedFacts { } )
if err != nil {
t . Fatalf ( "BuildDailyRenderContext() error = %v" , err )
}
if ctx . Modules . Metadata != nil || 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 )
}
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 )
}
}
2026-06-14 23:36:47 +00:00
func TestBuildTomorrowRenderContext ( t * testing . T ) {
metadata := testTomorrowMetadata ( )
snapshot := testTomorrowSnapshot ( t )
generated := Tomorrow {
Summary : "Tomorrow starts quiet, then showers become more likely later in the day." ,
ForecastDiscussion : [ ] string {
"Morning conditions should stay mostly dry." ,
"Rain chances increase during the afternoon as deeper moisture arrives." ,
} ,
PrecipitationTiming : "The most likely rain window is from midafternoon into early evening." ,
}
collected := testCollected ( )
derived := testTomorrowDerived ( )
ctx , err := BuildTomorrowRenderContext ( metadata , snapshot , generated , collected , derived )
if err != nil {
t . Fatalf ( "BuildTomorrowRenderContext() error = %v" , err )
}
if ctx . Report . Title != "Monday's Weather" {
t . Fatalf ( "Report.Title = %q, want Monday's Weather" , ctx . Report . Title )
}
if ctx . Report . ForecastDateLabel != "Monday, June 15, 2026" || ctx . Report . ForecastDayName != "Monday" {
t . Fatalf ( "forecast date labels = %q/%q, want Monday labels" , ctx . Report . ForecastDateLabel , ctx . Report . ForecastDayName )
}
if ctx . Report . GeneratedAtLabel != "Sunday, June 14, 2026 at 9:14 AM" {
t . Fatalf ( "Report.GeneratedAtLabel = %q, want friendly generated-at label" , ctx . Report . GeneratedAtLabel )
}
if ctx . Modules . Metadata == nil || ctx . Modules . Metadata . ReportID != report . Tomorrow {
t . Fatalf ( "Modules.Metadata = %#v, want tomorrow metadata" , ctx . Modules . Metadata )
}
if ctx . Modules . CurrentConditions == nil || ctx . Modules . HourlyForecast == nil {
t . Fatalf ( "current/hourly modules = %#v/%#v, want available in render context" , ctx . Modules . CurrentConditions , ctx . Modules . HourlyForecast )
}
if ctx . Modules . DerivedDailySummary == nil || ctx . Modules . DerivedDailySummary . HighTempF == nil || * ctx . Modules . DerivedDailySummary . HighTempF != 74 {
t . Fatalf ( "Modules.DerivedDailySummary = %#v, want daily summary" , ctx . Modules . DerivedDailySummary )
}
if ctx . Modules . DerivedDaypartSummaries == nil || len ( * ctx . Modules . DerivedDaypartSummaries ) != 2 {
t . Fatalf ( "Modules.DerivedDaypartSummaries = %#v, want daypart map" , ctx . Modules . DerivedDaypartSummaries )
}
if len ( ctx . Modules . Dayparts ) != 2 || ctx . Modules . Dayparts [ 0 ] . Key != "morning" || ctx . Modules . Dayparts [ 1 ] . Key != "afternoon" {
t . Fatalf ( "Modules.Dayparts = %#v, want configured order" , ctx . Modules . Dayparts )
}
2026-06-15 20:38:56 +00:00
if morning := ( * ctx . Modules . DerivedDaypartSummaries ) [ "morning" ] ; morning . DominantConditionDisplay == "" || morning . DominantConditionLower == "" || morning . TemperatureSteadyPhraseF == "" {
t . Fatalf ( "tomorrow rich morning daypart helpers = %#v, want render-context helper fields" , morning )
}
if afternoon := ( * ctx . Modules . DerivedDaypartSummaries ) [ "afternoon" ] ; afternoon . MaxPopTimeLabel != "3:00 PM" {
t . Fatalf ( "tomorrow rich afternoon daypart = %#v, want max pop time label helper" , afternoon )
}
2026-06-14 23:36:47 +00:00
if ctx . Modules . PrecipTiming == nil || len ( ctx . Modules . PrecipTiming . PrecipitationWindows ) != 1 {
t . Fatalf ( "Modules.PrecipTiming = %#v, want precipitation window" , ctx . Modules . PrecipTiming )
}
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 )
}
if ! ctx . Collected . FetchedAt . Equal ( collected . FetchedAt ) {
t . Fatalf ( "Collected.FetchedAt = %s, want %s" , ctx . Collected . FetchedAt , collected . FetchedAt )
}
if len ( ctx . Derived . DaypartSummaries ) != 2 {
t . Fatalf ( "Derived.DaypartSummaries = %#v, want passthrough facts" , ctx . Derived . DaypartSummaries )
}
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." ,
2026-06-14 23:26:45 -05:00
"- **Morning:** Partly cloudy, with temperatures in the low 60s." ,
"- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%." ,
2026-06-14 23:36:47 +00:00
"## Precipitation Timing" ,
"- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. 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 ) {
snapshot , err := module . NewSnapshot ( nil )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
ctx , err := BuildTomorrowRenderContext ( testTomorrowMetadata ( ) , snapshot , Tomorrow {
Summary : "Dry weather is expected tomorrow." ,
ForecastDiscussion : [ ] string { "High pressure keeps conditions quiet." } ,
} , testCollected ( ) , facts . DerivedFacts { } )
if err != nil {
t . Fatalf ( "BuildTomorrowRenderContext() error = %v" , err )
}
2026-06-15 12:41:45 +00:00
if ctx . Modules . Metadata != nil || ctx . Modules . CurrentConditions != nil || ctx . Modules . PrecipTiming != nil || len ( ctx . Modules . Dayparts ) != 0 {
2026-06-14 23:36:47 +00:00
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 )
}
}
2026-06-14 04:46:53 +00:00
func testMetadata ( ) briefing . Metadata {
generatedAt := time . Date ( 2026 , 5 , 29 , 13 , 30 , 0 , 0 , time . UTC )
return briefing . Metadata {
RunID : "run-1" ,
ReportID : report . Hourly ,
PromptID : "weather.hourly_generated_text" ,
GeneratedAt : generatedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : timeutil . Period {
Start : generatedAt ,
End : generatedAt . Add ( 6 * time . Hour ) ,
} ,
Location : & briefing . LocationContext {
Name : "Brentwood" ,
Region : "MO" ,
} ,
}
}
2026-06-14 23:36:47 +00:00
func testTomorrowMetadata ( ) briefing . Metadata {
generatedAt := time . Date ( 2026 , 6 , 14 , 14 , 14 , 0 , 0 , time . UTC )
return briefing . Metadata {
RunID : "run-tomorrow" ,
ReportID : report . Tomorrow ,
PromptID : "weather.tomorrow_generated_text" ,
GeneratedAt : generatedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : timeutil . Period {
Start : time . Date ( 2026 , 6 , 15 , 5 , 0 , 0 , 0 , time . UTC ) ,
End : time . Date ( 2026 , 6 , 16 , 5 , 0 , 0 , 0 , time . UTC ) ,
} ,
Location : & briefing . LocationContext {
Name : "Brentwood" ,
Region : "MO" ,
} ,
}
}
2026-06-15 16:29:18 +00:00
func testDailyMetadata ( ) briefing . Metadata {
generatedAt := time . Date ( 2026 , 6 , 13 , 14 , 14 , 0 , 0 , time . UTC )
return briefing . Metadata {
RunID : "run-daily" ,
2026-06-15 16:43:06 +00:00
ReportID : report . Daily ,
2026-06-15 16:29:18 +00:00
PromptID : "weather.daily_generated_text" ,
GeneratedAt : generatedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : timeutil . Period {
Start : time . Date ( 2026 , 6 , 15 , 5 , 0 , 0 , 0 , time . UTC ) ,
End : time . Date ( 2026 , 6 , 16 , 5 , 0 , 0 , 0 , time . UTC ) ,
} ,
Location : & briefing . LocationContext {
Name : "Brentwood" ,
Region : "MO" ,
} ,
}
}
2026-06-15 14:45:49 +00:00
func testTodayMetadata ( ) briefing . Metadata {
generatedAt := time . Date ( 2026 , 6 , 15 , 12 , 14 , 0 , 0 , time . UTC )
return briefing . Metadata {
RunID : "run-today" ,
ReportID : report . Today ,
PromptID : "weather.today_generated_text" ,
GeneratedAt : generatedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : timeutil . Period {
Start : time . Date ( 2026 , 6 , 15 , 5 , 0 , 0 , 0 , time . UTC ) ,
End : time . Date ( 2026 , 6 , 16 , 5 , 0 , 0 , 0 , time . UTC ) ,
} ,
Location : & briefing . LocationContext {
Name : "Brentwood" ,
Region : "MO" ,
} ,
}
}
2026-06-14 08:57:53 -05:00
func testCollected ( ) facts . CollectedFacts {
return facts . CollectedFacts { FetchedAt : time . Date ( 2026 , 5 , 29 , 13 , 31 , 0 , 0 , time . UTC ) }
}
func testDerived ( ) facts . DerivedFacts {
return facts . DerivedFacts { PrecipTiming : forecast . PrecipTiming { ThunderMentioned : true } }
}
2026-06-14 23:36:47 +00:00
func testTomorrowDerived ( ) facts . DerivedFacts {
2026-06-15 14:45:49 +00:00
return testCivilDayDerived ( )
}
2026-06-15 16:29:18 +00:00
func testDailyDerived ( ) facts . DerivedFacts {
return testCivilDayDerived ( )
}
2026-06-15 14:45:49 +00:00
func testTodayDerived ( ) facts . DerivedFacts {
return testCivilDayDerived ( )
}
func testCivilDayDerived ( ) facts . DerivedFacts {
2026-06-14 23:36:47 +00:00
morningStart := time . Date ( 2026 , 6 , 15 , 11 , 0 , 0 , 0 , time . UTC )
afternoonStart := time . Date ( 2026 , 6 , 15 , 17 , 0 , 0 , 0 , time . UTC )
return facts . DerivedFacts {
DaypartSummaries : [ ] forecast . DaypartSummary {
{
Name : "morning" ,
Period : timeutil . Period {
Start : morningStart ,
End : afternoonStart ,
} ,
} ,
{
Name : "afternoon" ,
Period : timeutil . Period {
Start : afternoonStart ,
End : time . Date ( 2026 , 6 , 15 , 23 , 0 , 0 , 0 , time . UTC ) ,
} ,
} ,
} ,
}
}
2026-06-15 14:45:49 +00:00
func testTodaySnapshot ( t * testing . T ) module . Snapshot {
t . Helper ( )
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{
ID : module . Metadata ,
StanzaName : string ( module . Metadata ) ,
Value : briefing . MetadataModule {
RunID : "run-today" ,
ReportID : report . Today ,
PromptID : "weather.today_generated_text" ,
GeneratedAt : testTodayMetadata ( ) . GeneratedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : testTodayMetadata ( ) . ValidPeriod ,
} ,
} ,
{
ID : module . CurrentConditions ,
StanzaName : string ( module . CurrentConditions ) ,
Value : briefing . CurrentConditionsModule {
2026-06-15 11:01:03 -05:00
ConditionTextLower : "clear" ,
TemperatureF : intPtr ( 58 ) ,
ApparentTemperatureF : intPtr ( 57 ) ,
RelativeHumidityPercent : intPtr ( 61 ) ,
WindSpeedMph : intPtr ( 9 ) ,
WindDirectionText : "northwest" ,
2026-06-15 14:45:49 +00:00
} ,
} ,
{
ID : module . HourlyForecast ,
StanzaName : string ( module . HourlyForecast ) ,
Value : briefing . HourlyForecastModule {
Periods : [ ] briefing . HourlyForecastPeriod {
{ HourLabel : "7:00 AM" , TextDescriptionLower : "partly cloudy" , TemperatureF : floatPtr ( 63 ) } ,
} ,
} ,
} ,
{
ID : module . DerivedDailySummary ,
StanzaName : string ( module . DerivedDailySummary ) ,
Value : briefing . DerivedDailySummaryModule {
Date : "Monday, June 15, 2026" ,
HighTempF : intPtr ( 74 ) ,
LowTempF : intPtr ( 61 ) ,
DailyPrecipitationProbability : intPtr ( 70 ) ,
} ,
} ,
{
ID : module . DerivedDaypartSummaries ,
StanzaName : string ( module . DerivedDaypartSummaries ) ,
Value : map [ string ] briefing . DerivedDaypartSummaryModule {
"afternoon" : {
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "mid 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
} ,
"morning" : {
DisplayName : "Morning" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "low 60s" ,
DominantConditionDisplay : "Partly cloudy" ,
DominantConditionLower : "partly cloudy" ,
} ,
} ,
} ,
{
ID : module . PrecipTiming ,
StanzaName : string ( module . PrecipTiming ) ,
Value : briefing . PrecipTimingModule {
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "3 PM" ,
PrecipitationWindows : [ ] briefing . PrecipitationWindowModule {
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" } ,
} ,
} ,
} ,
{
ID : module . AlertDigest ,
StanzaName : string ( module . AlertDigest ) ,
Value : briefing . AlertDigestModule {
Checked : true ,
} ,
} ,
{
ID : module . SPCConvectiveOutlooks ,
StanzaName : string ( module . SPCConvectiveOutlooks ) ,
Value : briefing . SPCConvectiveOutlooksModule {
Checked : true ,
} ,
} ,
{
ID : module . AreaForecastDiscussion ,
StanzaName : string ( module . AreaForecastDiscussion ) ,
Value : briefing . AreaForecastDiscussionModule {
KeyMessages : [ ] string { "Rain chances increase late." } ,
} ,
} ,
{
ID : module . SPCConvectiveDiscussion ,
StanzaName : string ( module . SPCConvectiveDiscussion ) ,
Value : briefing . SPCConvectiveDiscussionModule {
IncludedBecause : "convective outlook" ,
} ,
} ,
{
ID : module . WeatherStory ,
StanzaName : string ( module . WeatherStory ) ,
Value : briefing . WeatherStoryModule {
Available : true ,
Title : "Rain returns" ,
} ,
} ,
{
ID : module . TodayPlanning ,
StanzaName : string ( module . TodayPlanning ) ,
Value : briefing . TodayPlanningModule {
MorningReadiness : [ ] string { "Take sunglasses early." } ,
OutdoorPlanning : [ ] string { "Best outdoor window: Morning (quiet weather)." } ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
return snapshot
}
2026-06-15 16:29:18 +00:00
func testDailySnapshot ( t * testing . T ) module . Snapshot {
t . Helper ( )
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{
ID : module . Metadata ,
StanzaName : string ( module . Metadata ) ,
Value : briefing . MetadataModule {
RunID : "run-daily" ,
2026-06-15 16:43:06 +00:00
ReportID : report . Daily ,
2026-06-15 16:29:18 +00:00
PromptID : "weather.daily_generated_text" ,
GeneratedAt : testDailyMetadata ( ) . GeneratedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : testDailyMetadata ( ) . ValidPeriod ,
} ,
} ,
{
ID : module . CurrentConditions ,
StanzaName : string ( module . CurrentConditions ) ,
Value : briefing . CurrentConditionsModule {
ConditionTextLower : "clear" ,
TemperatureF : intPtr ( 58 ) ,
} ,
} ,
{
ID : module . HourlyForecast ,
StanzaName : string ( module . HourlyForecast ) ,
Value : briefing . HourlyForecastModule {
Periods : [ ] briefing . HourlyForecastPeriod {
{ HourLabel : "7:00 AM" , TextDescriptionLower : "partly cloudy" , TemperatureF : floatPtr ( 63 ) } ,
} ,
} ,
} ,
{
ID : module . DerivedDailySummary ,
StanzaName : string ( module . DerivedDailySummary ) ,
Value : briefing . DerivedDailySummaryModule {
Date : "Monday, June 15, 2026" ,
HighTempF : intPtr ( 74 ) ,
LowTempF : intPtr ( 61 ) ,
DailyPrecipitationProbability : intPtr ( 70 ) ,
} ,
} ,
{
ID : module . DerivedDaypartSummaries ,
StanzaName : string ( module . DerivedDaypartSummaries ) ,
Value : map [ string ] briefing . DerivedDaypartSummaryModule {
"afternoon" : {
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "mid 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
} ,
"evening" : {
DisplayName : "Evening" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 60s" ,
DominantConditionDisplay : "Mostly cloudy" ,
DominantConditionLower : "mostly cloudy" ,
} ,
"morning" : {
DisplayName : "Morning" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "low 60s" ,
DominantConditionDisplay : "Partly cloudy" ,
DominantConditionLower : "partly cloudy" ,
} ,
} ,
} ,
{
ID : module . PrecipTiming ,
StanzaName : string ( module . PrecipTiming ) ,
Value : briefing . PrecipTimingModule {
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "3 PM" ,
PrecipitationWindows : [ ] briefing . PrecipitationWindowModule {
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" } ,
} ,
} ,
} ,
{
ID : module . AlertDigest ,
StanzaName : string ( module . AlertDigest ) ,
Value : briefing . AlertDigestModule {
Checked : true ,
} ,
} ,
{
ID : module . SPCConvectiveOutlooks ,
StanzaName : string ( module . SPCConvectiveOutlooks ) ,
Value : briefing . SPCConvectiveOutlooksModule {
Checked : true ,
} ,
} ,
{
ID : module . AreaForecastDiscussion ,
StanzaName : string ( module . AreaForecastDiscussion ) ,
Value : briefing . AreaForecastDiscussionModule {
KeyMessages : [ ] string { "Rain chances increase late." } ,
} ,
} ,
{
ID : module . SPCConvectiveDiscussion ,
StanzaName : string ( module . SPCConvectiveDiscussion ) ,
Value : briefing . SPCConvectiveDiscussionModule {
IncludedBecause : "convective outlook" ,
} ,
} ,
{
ID : module . WeatherStory ,
StanzaName : string ( module . WeatherStory ) ,
Value : briefing . WeatherStoryModule {
Available : true ,
Title : "Rain returns" ,
} ,
} ,
{
ID : module . OutdoorWindows ,
StanzaName : string ( module . OutdoorWindows ) ,
Value : briefing . OutdoorWindowsModule {
Best : & briefing . OutdoorWindowModule {
Daypart : "morning" ,
Reasons : [ ] string { "quiet weather" } ,
} ,
Worst : & briefing . OutdoorWindowModule {
Daypart : "afternoon" ,
Reasons : [ ] string { "high precipitation chance" } ,
} ,
} ,
} ,
{
ID : module . DailyPlanning ,
StanzaName : string ( module . DailyPlanning ) ,
Value : briefing . DailyPlanningModule {
MorningReadiness : [ ] string { "Take sunglasses early." } ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
return snapshot
}
2026-06-14 04:46:53 +00:00
func testSnapshot ( t * testing . T ) module . Snapshot {
t . Helper ( )
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{
ID : module . CurrentConditions ,
StanzaName : string ( module . CurrentConditions ) ,
Value : briefing . CurrentConditionsModule {
ConditionText : "Partly cloudy" ,
2026-06-14 12:54:32 -05:00
ConditionTextLower : "partly cloudy" ,
2026-06-14 17:40:19 -05:00
TemperatureF : intPtr ( 74 ) ,
ApparentTemperatureF : intPtr ( 76 ) ,
RelativeHumidityPercent : intPtr ( 71 ) ,
WindSpeedMph : intPtr ( 8 ) ,
2026-06-14 04:46:53 +00:00
WindDirection : "S" ,
2026-06-14 12:54:32 -05:00
WindDirectionText : "south" ,
2026-06-14 04:46:53 +00:00
} ,
} ,
{
ID : module . HourlyForecast ,
StanzaName : string ( module . HourlyForecast ) ,
Value : briefing . HourlyForecastModule {
Periods : [ ] briefing . HourlyForecastPeriod {
{
2026-06-14 12:54:32 -05:00
HourLabel : "9:00 AM" ,
2026-06-14 04:46:53 +00:00
PeriodBegins : "2026-05-29 at 9:00 AM" ,
TextDescription : "Cloudy" ,
2026-06-14 12:54:32 -05:00
TextDescriptionLower : "cloudy" ,
2026-06-14 04:46:53 +00:00
TemperatureF : floatPtr ( 74 ) ,
ProbabilityOfPrecipitationPercent : floatPtr ( 30 ) ,
2026-06-14 12:54:32 -05:00
MentionPrecipitation : true ,
2026-06-14 04:46:53 +00:00
WindSpeedMph : floatPtr ( 8 ) ,
WindDirection : "S" ,
} ,
{
2026-06-14 12:54:32 -05:00
HourLabel : "10:00 AM" ,
2026-06-14 04:46:53 +00:00
PeriodBegins : "2026-05-29 at 10:00 AM" ,
TextDescription : "Showers" ,
2026-06-14 12:54:32 -05:00
TextDescriptionLower : "showers" ,
2026-06-14 04:46:53 +00:00
TemperatureF : floatPtr ( 75 ) ,
ProbabilityOfPrecipitationPercent : floatPtr ( 70 ) ,
2026-06-14 12:54:32 -05:00
MentionPrecipitation : true ,
2026-06-14 04:46:53 +00:00
WindSpeedMph : floatPtr ( 10 ) ,
WindGustMph : floatPtr ( 18 ) ,
WindDirection : "S" ,
} ,
} ,
} ,
} ,
{
ID : module . PrecipTiming ,
StanzaName : string ( module . PrecipTiming ) ,
Value : briefing . PrecipTimingModule {
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "10 AM" ,
ProbabilityThreshold : 50 ,
PrecipitationWindows : [ ] briefing . PrecipitationWindowModule {
{
2026-06-14 12:54:32 -05:00
PeriodBegins : "2026-05-29 at 10:00 AM" ,
PeriodBeginsHourLabel : "10:00 AM" ,
PeriodEnds : "2026-05-29 at 12:00 PM" ,
PeriodEndsHourLabel : "12:00 PM" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "10 AM" ,
MaxPopHourLabel : "10:00 AM" ,
2026-06-14 04:46:53 +00:00
} ,
} ,
ThunderMentioned : true ,
} ,
} ,
{
ID : module . AlertDigest ,
StanzaName : string ( module . AlertDigest ) ,
Value : briefing . AlertDigestModule {
Checked : true ,
ActiveCount : 1 ,
RelevantCount : 1 ,
Relevant : [ ] briefing . AlertSummary {
{ Event : "Flood Watch" , Headline : "Flood Watch until early afternoon" , Severity : "Moderate" } ,
} ,
} ,
} ,
{
ID : module . SPCConvectiveOutlooks ,
StanzaName : string ( module . SPCConvectiveOutlooks ) ,
Value : briefing . SPCConvectiveOutlooksModule {
Checked : true ,
OutlookCount : 1 ,
Outlooks : [ ] briefing . SPCConvectiveOutlookRecord {
{
Label : "SLGT" ,
LabelText : "Slight Risk" ,
PeriodBegins : "2026-05-29 at 7:00 AM" ,
PeriodEnds : "2026-05-29 at 3:00 PM" ,
} ,
} ,
} ,
} ,
{
ID : module . AreaForecastDiscussion ,
StanzaName : string ( module . AreaForecastDiscussion ) ,
Value : briefing . AreaForecastDiscussionModule {
KeyMessages : [ ] string { "Storms are most likely late morning." } ,
ShortTerm : "Short-term discussion favors increasing rain coverage." ,
} ,
} ,
{
ID : module . SPCConvectiveDiscussion ,
StanzaName : string ( module . SPCConvectiveDiscussion ) ,
Value : briefing . SPCConvectiveDiscussionModule {
IncludedBecause : "categorical severity_rank >= 3" ,
Discussions : [ ] briefing . SPCConvectiveDiscussionRecord {
{ Headline : "Mesoscale discussion" , Summary : "Strong storms may develop late morning." } ,
} ,
} ,
} ,
{
ID : module . WeatherStory ,
StanzaName : string ( module . WeatherStory ) ,
Value : briefing . WeatherStoryModule {
Available : true ,
Title : "Morning storms" ,
Description : "Morning storms remain the main story." ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
return snapshot
}
2026-06-14 23:36:47 +00:00
func testTomorrowSnapshot ( t * testing . T ) module . Snapshot {
t . Helper ( )
snapshot , err := module . NewSnapshot ( [ ] module . Output {
{
ID : module . Metadata ,
StanzaName : string ( module . Metadata ) ,
Value : briefing . MetadataModule {
RunID : "run-tomorrow" ,
ReportID : report . Tomorrow ,
PromptID : "weather.tomorrow_generated_text" ,
GeneratedAt : testTomorrowMetadata ( ) . GeneratedAt ,
Units : "imperial" ,
Timezone : "America/Chicago" ,
ValidPeriod : testTomorrowMetadata ( ) . ValidPeriod ,
} ,
} ,
{
ID : module . CurrentConditions ,
StanzaName : string ( module . CurrentConditions ) ,
Value : briefing . CurrentConditionsModule {
ConditionTextLower : "clear" ,
TemperatureF : intPtr ( 58 ) ,
} ,
} ,
{
ID : module . HourlyForecast ,
StanzaName : string ( module . HourlyForecast ) ,
Value : briefing . HourlyForecastModule {
Periods : [ ] briefing . HourlyForecastPeriod {
{ HourLabel : "7:00 AM" , TextDescriptionLower : "partly cloudy" , TemperatureF : floatPtr ( 63 ) } ,
} ,
} ,
} ,
{
ID : module . DerivedDailySummary ,
StanzaName : string ( module . DerivedDailySummary ) ,
Value : briefing . DerivedDailySummaryModule {
Date : "Monday, June 15, 2026" ,
HighTempF : intPtr ( 74 ) ,
LowTempF : intPtr ( 61 ) ,
DailyPrecipitationProbability : intPtr ( 70 ) ,
} ,
} ,
{
ID : module . DerivedDaypartSummaries ,
StanzaName : string ( module . DerivedDaypartSummaries ) ,
Value : map [ string ] briefing . DerivedDaypartSummaryModule {
"afternoon" : {
2026-06-14 23:26:45 -05:00
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "mid 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
2026-06-14 23:36:47 +00:00
} ,
"morning" : {
2026-06-14 23:26:45 -05:00
DisplayName : "Morning" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "low 60s" ,
DominantConditionDisplay : "Partly cloudy" ,
DominantConditionLower : "partly cloudy" ,
2026-06-14 23:36:47 +00:00
} ,
} ,
} ,
{
ID : module . PrecipTiming ,
StanzaName : string ( module . PrecipTiming ) ,
Value : briefing . PrecipTimingModule {
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "3 PM" ,
PrecipitationWindows : [ ] briefing . PrecipitationWindowModule {
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" } ,
} ,
} ,
} ,
{
ID : module . AlertDigest ,
StanzaName : string ( module . AlertDigest ) ,
Value : briefing . AlertDigestModule {
Checked : true ,
} ,
} ,
{
ID : module . SPCConvectiveOutlooks ,
StanzaName : string ( module . SPCConvectiveOutlooks ) ,
Value : briefing . SPCConvectiveOutlooksModule {
Checked : true ,
} ,
} ,
{
ID : module . AreaForecastDiscussion ,
StanzaName : string ( module . AreaForecastDiscussion ) ,
Value : briefing . AreaForecastDiscussionModule {
KeyMessages : [ ] string { "Rain chances increase late." } ,
} ,
} ,
{
ID : module . SPCConvectiveDiscussion ,
StanzaName : string ( module . SPCConvectiveDiscussion ) ,
Value : briefing . SPCConvectiveDiscussionModule {
IncludedBecause : "convective outlook" ,
} ,
} ,
{
ID : module . WeatherStory ,
StanzaName : string ( module . WeatherStory ) ,
Value : briefing . WeatherStoryModule {
Available : true ,
Title : "Rain returns" ,
} ,
} ,
{
ID : module . TomorrowPlanning ,
StanzaName : string ( module . TomorrowPlanning ) ,
Value : briefing . TomorrowPlanningModule {
MorningReadiness : [ ] string { "Take sunglasses early." } ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "NewSnapshot() error = %v" , err )
}
return snapshot
}
2026-06-14 04:46:53 +00:00
func floatPtr ( value float64 ) * float64 {
return & value
}
func intPtr ( value int ) * int {
return & value
}
2026-06-14 23:36:47 +00:00
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
}
}