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." ,
2026-06-14 04:46:53 +00:00
}
2026-08-13 02:11:25 +00:00
ctx , err := BuildHourlyRenderContext ( metadata , snapshot , generated )
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 )
}
2026-06-14 04:46:53 +00:00
}
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-08-13 02:11:25 +00:00
} )
2026-06-14 08:57:53 -05:00
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." ,
2026-08-13 02:11:25 +00:00
} )
2026-06-15 12:41:45 +00:00
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-08-13 02:11:25 +00:00
func TestRenderContextsRejectConflictingIdentity ( t * testing . T ) {
tests := [ ] struct {
name string
build func ( ) error
} {
{
name : "hourly" ,
build : func ( ) error {
identity := testMetadata ( )
identity . ReportID = report . Daily
_ , err := BuildHourlyRenderContext ( identity , testSnapshot ( t ) , Hourly { } )
return err
} ,
} ,
{
name : "daily" ,
build : func ( ) error {
identity := testDailyMetadata ( )
identity . ReportID = report . Today
_ , err := BuildDailyRenderContext ( identity , testDailySnapshot ( t ) , Daily { } , testDailyDerived ( ) )
return err
} ,
} ,
{
name : "today" ,
build : func ( ) error {
identity := testTodayMetadata ( )
identity . ReportID = report . Tomorrow
_ , err := BuildTodayRenderContext ( identity , testTodaySnapshot ( t ) , Today { } , testTodayDerived ( ) )
return err
} ,
} ,
{
name : "tomorrow" ,
build : func ( ) error {
identity := testTomorrowMetadata ( )
identity . ReportID = report . Hourly
_ , err := BuildTomorrowRenderContext ( identity , testTomorrowSnapshot ( t ) , Tomorrow { } , testTomorrowDerived ( ) )
return err
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
err := test . build ( )
if err == nil {
t . Fatal ( "render context error = nil, want conflicting identity rejection" )
}
if ! strings . Contains ( err . Error ( ) , "render identity report" ) {
t . Fatalf ( "render context error = %v, want identity mismatch" , err )
}
} )
}
}
func TestRenderContextRejectsMetadataModuleMismatch ( t * testing . T ) {
identity := testTodayMetadata ( )
snapshot := testTodaySnapshot ( t )
for index := range snapshot . Outputs {
if snapshot . Outputs [ index ] . ID != module . Metadata {
continue
}
metadata := snapshot . Outputs [ index ] . Value . ( briefing . MetadataModule )
metadata . PromptID = "weather.daily_generated_text"
snapshot . Outputs [ index ] . Value = metadata
}
_ , err := BuildTodayRenderContext ( identity , snapshot , Today { } , testTodayDerived ( ) )
if err == nil {
t . Fatal ( "BuildTodayRenderContext() error = nil, want metadata module mismatch" )
}
if ! strings . Contains ( err . Error ( ) , "metadata module does not match render identity" ) {
t . Fatalf ( "BuildTodayRenderContext() error = %v, want metadata mismatch" , err )
}
}
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." ,
}
derived := testTodayDerived ( )
2026-08-13 02:11:25 +00:00
ctx , err := BuildTodayRenderContext ( metadata , snapshot , generated , derived )
2026-06-15 14:45:49 +00:00
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 . 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-08-13 02:11:25 +00:00
if ! ctx . Modules . HasDaypartDetails {
t . Fatal ( "Modules.HasDaypartDetails = false, want displayable 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 )
}
}
2026-08-13 01:08:20 +00:00
func TestTodayRenderContextPreservesUnicodeDaypartDisplayNames ( t * testing . T ) {
metadata := testTodayMetadata ( )
snapshot := testTodaySnapshot ( t )
for i := range snapshot . Outputs {
if snapshot . Outputs [ i ] . ID != module . DerivedDaypartSummaries {
continue
}
dayparts := snapshot . Outputs [ i ] . Value . ( map [ string ] briefing . DerivedDaypartSummaryModule )
morning := dayparts [ "morning" ]
morning . DisplayName = "Mañana"
dayparts [ "morning" ] = morning
}
ctx , err := BuildTodayRenderContext ( metadata , snapshot , Today {
Summary : "Today summary." ,
ForecastDiscussion : [ ] string { "Morning conditions stay quiet." } ,
2026-08-13 02:11:25 +00:00
} , testTodayDerived ( ) )
2026-08-13 01:08:20 +00:00
if err != nil {
t . Fatalf ( "BuildTodayRenderContext() error = %v" , err )
}
2026-08-13 04:22:33 +00:00
if len ( ctx . Modules . Dayparts ) == 0 || ctx . Modules . Dayparts [ 0 ] . Summary . DisplayName != "Mañana" {
t . Fatalf ( "dayparts = %#v, want Unicode daypart display name" , ctx . Modules . Dayparts )
2026-08-13 01:08:20 +00:00
}
}
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." } ,
2026-08-13 02:11:25 +00:00
} , facts . DerivedFacts { } )
2026-06-15 14:45:49 +00:00
if err != nil {
t . Fatalf ( "BuildTodayRenderContext() error = %v" , err )
}
2026-08-13 02:11:25 +00:00
if ctx . Modules . CurrentConditions != nil || ctx . Modules . PrecipTiming != nil || len ( ctx . Modules . Dayparts ) != 0 || ctx . Modules . TodayPlanning != nil {
2026-06-15 14:45:49 +00:00
t . Fatalf ( "Modules = %#v, want omitted optional modules" , ctx . Modules )
}
2026-08-13 02:11:25 +00:00
if ctx . Modules . HasDaypartDetails {
t . Fatal ( "Modules.HasDaypartDetails = true, want no displayable dayparts" )
}
2026-06-15 14:45:49 +00:00
}
2026-06-16 15:03:35 +00:00
func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules ( t * testing . T ) {
type result struct {
title string
forecastDateLabel string
generatedAtLabel string
timezone string
validPeriod timeutil . Period
current bool
hourly bool
dailySummary bool
daypartSummaries bool
dayparts int
precip bool
alerts bool
outlooks bool
discussion bool
spcDiscussion bool
story bool
planning bool
}
tests := [ ] struct {
name string
wantTitle string
wantGeneratedLabel string
wantDayparts int
build func ( t * testing . T ) result
} {
{
name : "daily" ,
wantTitle : "Monday's Weather" ,
wantGeneratedLabel : "Saturday, June 13, 2026 at 9:14 AM" ,
wantDayparts : 3 ,
build : func ( t * testing . T ) result {
t . Helper ( )
derived := testDailyDerived ( )
ctx , err := BuildDailyRenderContext ( testDailyMetadata ( ) , testDailySnapshot ( t ) , Daily {
Summary : "Daily summary." ,
ForecastDiscussion : [ ] string { "Daily discussion." } ,
2026-08-13 02:11:25 +00:00
} , derived )
2026-06-16 15:03:35 +00:00
if err != nil {
t . Fatalf ( "BuildDailyRenderContext() error = %v" , err )
}
return result {
title : ctx . Report . Title ,
forecastDateLabel : ctx . Report . ForecastDateLabel ,
generatedAtLabel : ctx . Report . GeneratedAtLabel ,
timezone : ctx . Report . Timezone ,
validPeriod : ctx . Report . ValidPeriod ,
current : ctx . Modules . CurrentConditions != nil ,
hourly : ctx . Modules . HourlyForecast != nil ,
dailySummary : ctx . Modules . DerivedDailySummary != nil ,
daypartSummaries : ctx . Modules . DerivedDaypartSummaries != nil ,
dayparts : len ( ctx . Modules . Dayparts ) ,
precip : ctx . Modules . PrecipTiming != nil ,
alerts : ctx . Modules . AlertDigest != nil ,
outlooks : ctx . Modules . SPCConvectiveOutlooks != nil ,
discussion : ctx . Modules . AreaForecastDiscussion != nil ,
spcDiscussion : ctx . Modules . SPCConvectiveDiscussion != nil ,
story : ctx . Modules . WeatherStory != nil ,
planning : ctx . Modules . DailyPlanning != nil ,
}
} ,
} ,
{
name : "today" ,
wantTitle : "Today's Weather" ,
wantGeneratedLabel : "Monday, June 15, 2026 at 7:14 AM" ,
wantDayparts : 2 ,
build : func ( t * testing . T ) result {
t . Helper ( )
derived := testTodayDerived ( )
ctx , err := BuildTodayRenderContext ( testTodayMetadata ( ) , testTodaySnapshot ( t ) , Today {
Summary : "Today summary." ,
ForecastDiscussion : [ ] string { "Today discussion." } ,
2026-08-13 02:11:25 +00:00
} , derived )
2026-06-16 15:03:35 +00:00
if err != nil {
t . Fatalf ( "BuildTodayRenderContext() error = %v" , err )
}
return result {
title : ctx . Report . Title ,
forecastDateLabel : ctx . Report . ForecastDateLabel ,
generatedAtLabel : ctx . Report . GeneratedAtLabel ,
timezone : ctx . Report . Timezone ,
validPeriod : ctx . Report . ValidPeriod ,
current : ctx . Modules . CurrentConditions != nil ,
hourly : ctx . Modules . HourlyForecast != nil ,
dailySummary : ctx . Modules . DerivedDailySummary != nil ,
daypartSummaries : ctx . Modules . DerivedDaypartSummaries != nil ,
dayparts : len ( ctx . Modules . Dayparts ) ,
precip : ctx . Modules . PrecipTiming != nil ,
alerts : ctx . Modules . AlertDigest != nil ,
outlooks : ctx . Modules . SPCConvectiveOutlooks != nil ,
discussion : ctx . Modules . AreaForecastDiscussion != nil ,
spcDiscussion : ctx . Modules . SPCConvectiveDiscussion != nil ,
story : ctx . Modules . WeatherStory != nil ,
planning : ctx . Modules . TodayPlanning != nil ,
}
} ,
} ,
{
name : "tomorrow" ,
wantTitle : "Monday's Weather" ,
wantGeneratedLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
wantDayparts : 2 ,
build : func ( t * testing . T ) result {
t . Helper ( )
derived := testTomorrowDerived ( )
ctx , err := BuildTomorrowRenderContext ( testTomorrowMetadata ( ) , testTomorrowSnapshot ( t ) , Tomorrow {
Summary : "Tomorrow summary." ,
ForecastDiscussion : [ ] string { "Tomorrow discussion." } ,
2026-08-13 02:11:25 +00:00
} , derived )
2026-06-16 15:03:35 +00:00
if err != nil {
t . Fatalf ( "BuildTomorrowRenderContext() error = %v" , err )
}
return result {
title : ctx . Report . Title ,
forecastDateLabel : ctx . Report . ForecastDateLabel ,
generatedAtLabel : ctx . Report . GeneratedAtLabel ,
timezone : ctx . Report . Timezone ,
validPeriod : ctx . Report . ValidPeriod ,
current : ctx . Modules . CurrentConditions != nil ,
hourly : ctx . Modules . HourlyForecast != nil ,
dailySummary : ctx . Modules . DerivedDailySummary != nil ,
daypartSummaries : ctx . Modules . DerivedDaypartSummaries != nil ,
dayparts : len ( ctx . Modules . Dayparts ) ,
precip : ctx . Modules . PrecipTiming != nil ,
alerts : ctx . Modules . AlertDigest != nil ,
outlooks : ctx . Modules . SPCConvectiveOutlooks != nil ,
discussion : ctx . Modules . AreaForecastDiscussion != nil ,
spcDiscussion : ctx . Modules . SPCConvectiveDiscussion != nil ,
story : ctx . Modules . WeatherStory != nil ,
planning : ctx . Modules . TomorrowPlanning != nil ,
}
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
result := test . build ( t )
if result . title != test . wantTitle {
t . Fatalf ( "title = %q, want %q" , result . title , test . wantTitle )
}
if result . forecastDateLabel != "Monday, June 15, 2026" {
t . Fatalf ( "forecastDateLabel = %q, want Monday, June 15, 2026" , result . forecastDateLabel )
}
if result . generatedAtLabel != test . wantGeneratedLabel {
t . Fatalf ( "generatedAtLabel = %q, want %q" , result . generatedAtLabel , test . wantGeneratedLabel )
}
if result . timezone != "America/Chicago" {
t . Fatalf ( "timezone = %q, want America/Chicago" , result . timezone )
}
if ! result . validPeriod . IsValid ( ) {
t . Fatalf ( "validPeriod = %#v, want valid period" , result . validPeriod )
}
2026-08-13 02:11:25 +00:00
if ! result . current || ! result . hourly || ! result . dailySummary || ! result . daypartSummaries || ! result . precip || ! result . alerts || ! result . outlooks || ! result . discussion || ! result . spcDiscussion || ! result . story {
2026-06-16 15:03:35 +00:00
t . Fatalf ( "common modules = %#v, want all shared modules populated" , result )
}
if result . dayparts != test . wantDayparts {
t . Fatalf ( "dayparts = %d, want %d" , result . dayparts , test . wantDayparts )
}
if ! result . planning {
t . Fatalf ( "planning = false, want %s planning module populated" , test . name )
}
} )
}
}
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." ,
}
derived := testDailyDerived ( )
2026-08-13 02:11:25 +00:00
ctx , err := BuildDailyRenderContext ( metadata , snapshot , generated , derived )
2026-06-15 16:29:18 +00:00
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 )
}
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 )
}
}
2026-08-13 04:22:33 +00:00
func TestValidatedGeneratedTextBuildsAndRendersTypedContext ( t * testing . T ) {
generated , err := ValidateDaily ( [ ] byte ( ` { "summary":"Daily summary.","forecast_discussion":["Daily discussion."],"precipitation_timing":"Late-day showers."} ` ) )
2026-08-13 02:17:11 +00:00
if err != nil {
t . Fatalf ( "ValidateDaily() error = %v" , err )
}
context , err := BuildDailyRenderContext ( testDailyMetadata ( ) , testDailySnapshot ( t ) , generated , testDailyDerived ( ) )
if err != nil {
t . Fatalf ( "BuildDailyRenderContext() error = %v" , err )
}
rendered , err := reporttemplate . Render ( "daily" , context )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
2026-08-13 04:22:33 +00:00
for _ , want := range [ ] string { "# Monday's Weather" , "Daily summary." , "Daily discussion." , "Late-day showers." } {
if ! strings . Contains ( string ( rendered ) , want ) {
t . Fatalf ( "rendered report missing %q:\n%s" , want , rendered )
2026-08-13 02:17:11 +00:00
}
}
}
2026-06-15 16:29:18 +00:00
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." } ,
2026-08-13 02:11:25 +00:00
} , facts . DerivedFacts { } )
2026-06-15 16:29:18 +00:00
if err != nil {
t . Fatalf ( "BuildDailyRenderContext() error = %v" , err )
}
2026-08-13 02:11:25 +00:00
if ctx . Modules . CurrentConditions != nil || ctx . Modules . PrecipTiming != nil || ctx . Modules . OutdoorWindows != nil || ctx . Modules . DailyPlanning != nil || len ( ctx . Modules . Dayparts ) != 0 {
2026-06-15 16:29:18 +00:00
t . Fatalf ( "Modules = %#v, want omitted optional modules" , ctx . Modules )
}
}
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." ,
}
derived := testTomorrowDerived ( )
2026-08-13 02:11:25 +00:00
ctx , err := BuildTomorrowRenderContext ( metadata , snapshot , generated , derived )
2026-06-14 23:36:47 +00:00
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 . 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 )
}
}
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." } ,
2026-08-13 02:11:25 +00:00
} , facts . DerivedFacts { } )
2026-06-14 23:36:47 +00:00
if err != nil {
t . Fatalf ( "BuildTomorrowRenderContext() error = %v" , err )
}
2026-08-13 02:11:25 +00:00
if 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 )
}
}
2026-08-13 02:00:18 +00:00
func testMetadata ( ) briefing . PreparedIdentity {
2026-06-14 04:46:53 +00:00
generatedAt := time . Date ( 2026 , 5 , 29 , 13 , 30 , 0 , 0 , time . UTC )
2026-08-13 02:00:18 +00:00
return briefing . PreparedIdentity {
2026-06-14 04:46:53 +00:00
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-08-13 02:00:18 +00:00
func testTomorrowMetadata ( ) briefing . PreparedIdentity {
2026-06-14 23:36:47 +00:00
generatedAt := time . Date ( 2026 , 6 , 14 , 14 , 14 , 0 , 0 , time . UTC )
2026-08-13 02:00:18 +00:00
return briefing . PreparedIdentity {
2026-06-14 23:36:47 +00:00
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-08-13 02:00:18 +00:00
func testDailyMetadata ( ) briefing . PreparedIdentity {
2026-06-15 16:29:18 +00:00
generatedAt := time . Date ( 2026 , 6 , 13 , 14 , 14 , 0 , 0 , time . UTC )
2026-08-13 02:00:18 +00:00
return briefing . PreparedIdentity {
2026-06-15 16:29:18 +00:00
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-08-13 02:00:18 +00:00
func testTodayMetadata ( ) briefing . PreparedIdentity {
2026-06-15 14:45:49 +00:00
generatedAt := time . Date ( 2026 , 6 , 15 , 12 , 14 , 0 , 0 , time . UTC )
2026-08-13 02:00:18 +00:00
return briefing . PreparedIdentity {
2026-06-15 14:45:49 +00:00
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 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 ,
2026-08-13 02:11:25 +00:00
Location : testTodayMetadata ( ) . Location ,
2026-06-15 14:45:49 +00:00
} ,
} ,
{
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 {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , PrecipitationType : "showers" , ExpectationPhrase : "Expect showers." } ,
2026-06-15 14:45:49 +00:00
} ,
} ,
} ,
{
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 ,
2026-08-13 02:11:25 +00:00
Location : testDailyMetadata ( ) . Location ,
2026-06-15 16:29:18 +00:00
} ,
} ,
{
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 {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , PrecipitationType : "showers" , ExpectationPhrase : "Expect showers." } ,
2026-06-15 16:29:18 +00:00
} ,
} ,
} ,
{
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 {
2026-06-16 21:09:17 -05:00
{ Event : "Flood Watch" , Headline : "Flood Watch until early afternoon" , Severity : "Moderate" , PeriodBegins : "May 29 at 10:00 AM" , PeriodEnds : "May 29 at 2:30 PM" , Instruction : "Avoid low-water crossings." } ,
2026-06-14 04:46:53 +00:00
} ,
} ,
} ,
{
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 ,
2026-08-13 02:11:25 +00:00
Location : testTomorrowMetadata ( ) . Location ,
2026-06-14 23:36:47 +00:00
} ,
} ,
{
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 {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , PrecipitationType : "showers" , ExpectationPhrase : "Expect showers." } ,
2026-06-14 23:36:47 +00:00
} ,
} ,
} ,
{
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
}