2026-06-12 15:05:42 +00:00
package briefing
import (
"encoding/json"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
func TestSPCConvectiveDiscussionModuleOmitsBelowThresholdButOutlookRemains ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
ctx := spcConvectiveDiscussionContext ( 2 , [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 1 , "Lower risk" , "General thunderstorms." , "No organized severe weather is expected." , "2026-05-29T08:30:00-05:00" ) ,
} )
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule(discussion) error = %v" , err )
}
if output != nil {
t . Fatalf ( "discussion output = %#v, want omitted below threshold" , output )
}
outlookOutput , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveOutlooks } )
if err != nil {
t . Fatalf ( "BuildModule(outlooks) error = %v" , err )
}
outlookValue := moduleValue [ SPCConvectiveOutlooksModule ] ( t , outlookOutput )
if ! outlookValue . Checked || outlookValue . OutlookCount != 1 {
t . Fatalf ( "outlook value = %#v, want lower-risk outlook still emitted" , outlookValue )
}
}
func TestSPCConvectiveDiscussionModuleIncludesEqualThresholdDiscussion ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
ctx := spcConvectiveDiscussionContext ( 3 , [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 1 , "Severe storms possible" , "Scattered severe storms are possible." , "A few storms may become severe during the afternoon." , "2026-05-29T08:30:00-05:00" ) ,
} )
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
if output == nil || output . ID != module . SPCConvectiveDiscussion || output . StanzaName != "spc_convective_discussion" {
t . Fatalf ( "output = %#v, want spc convective discussion stanza" , output )
}
value := moduleValue [ SPCConvectiveDiscussionModule ] ( t , output )
2026-06-12 12:00:40 -05:00
if value . IncludedBecause != "categorical severity_rank >= 3" || len ( value . Discussions ) != 1 {
2026-06-12 15:05:42 +00:00
t . Fatalf ( "value = %#v, want threshold reason and one discussion" , value )
}
discussion := value . Discussions [ 0 ]
if discussion . Day != 1 || discussion . Headline != "Severe storms possible" || discussion . Summary == "" || discussion . Discussion == "" {
t . Fatalf ( "discussion = %#v, want prompt-facing discussion fields" , discussion )
}
2026-06-12 12:00:40 -05:00
if discussion . PeriodBegins != "2026-05-29 at 11:00 AM" || discussion . PeriodEnds != "2026-05-30 at 7:00 AM" {
t . Fatalf ( "Period = %q/%q, want qualifying outlook valid period" , discussion . PeriodBegins , discussion . PeriodEnds )
}
2026-06-12 15:05:42 +00:00
if discussion . UpdatedAt != "2026-05-29 at 8:30 AM" {
t . Fatalf ( "UpdatedAt = %q, want friendly local time" , discussion . UpdatedAt )
}
data , err := json . Marshal ( output . Value )
if err != nil {
t . Fatalf ( "Marshal() error = %v" , err )
}
text := string ( data )
2026-06-12 12:00:40 -05:00
for _ , field := range [ ] string { "included_because" , "discussions" , "period_begins" , "period_ends" , "headline" , "summary" , "discussion" , "updated_at" } {
2026-06-12 15:05:42 +00:00
if ! strings . Contains ( text , field ) {
t . Fatalf ( "json = %s, want field %s" , text , field )
}
}
2026-06-12 12:00:40 -05:00
if strings . Contains ( text , ` "period": ` ) {
t . Fatalf ( "json = %s, want period_begins/period_ends instead of period" , text )
}
2026-06-12 15:05:42 +00:00
}
func TestSPCConvectiveDiscussionModuleIncludesAboveThresholdDiscussion ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
ctx := spcConvectiveDiscussionContext ( 4 , [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 1 , "Enhanced severe risk" , "Numerous severe storms are possible." , "Severe storms may produce damaging winds." , "2026-05-29T09:15:00-05:00" ) ,
} )
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
value := moduleValue [ SPCConvectiveDiscussionModule ] ( t , output )
if len ( value . Discussions ) != 1 || value . Discussions [ 0 ] . Headline != "Enhanced severe risk" {
t . Fatalf ( "value = %#v, want above-threshold discussion" , value )
}
}
2026-06-12 12:00:40 -05:00
func TestSPCConvectiveDiscussionModuleIgnoresHighRankNonCategoricalOutlook ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
rank := 30
ctx := testModuleContext ( )
outlook := weatherdata . ConvectiveOutlook {
ID : "day1-wind-30" ,
Day : 1 ,
OutlookType : "wind" ,
Label : "30%" ,
LabelText : "30% Wind Risk" ,
SeverityRank : & rank ,
ValidFrom : mustParseModuleTime ( "2026-05-29T11:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
}
ctx . Collected . SPCConvectiveOutlooks = & weatherdata . ConvectiveOutlookRun {
Outlooks : [ ] weatherdata . ConvectiveOutlook { outlook } ,
}
ctx . Derived . SPCConvectiveOutlooks = [ ] weatherdata . ConvectiveOutlook { outlook }
ctx . Derived . SPCConvectiveDiscussions = [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 1 , "Wind risk discussion" , "High wind probabilities." , "This discussion should not be emitted from wind severity rank." , "2026-05-29T08:30:00-05:00" ) ,
}
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
if output != nil {
t . Fatalf ( "output = %#v, want non-categorical outlook ignored for discussion threshold" , output )
}
}
func TestSPCConvectiveDiscussionModuleRequiresCategoricalThresholdForMixedSameDayOutlooks ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
rank2 := 2
rank30 := 30
ctx := testModuleContext ( )
categorical := weatherdata . ConvectiveOutlook {
ID : "day1-marginal" ,
Day : 1 ,
OutlookType : "categorical" ,
Label : "MRGL" ,
LabelText : "Marginal Risk" ,
SeverityRank : & rank2 ,
ValidFrom : mustParseModuleTime ( "2026-05-29T11:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
}
wind := weatherdata . ConvectiveOutlook {
ID : "day1-wind-30" ,
Day : 1 ,
OutlookType : "wind" ,
Label : "30%" ,
LabelText : "30% Wind Risk" ,
SeverityRank : & rank30 ,
ValidFrom : mustParseModuleTime ( "2026-05-29T11:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
}
ctx . Collected . SPCConvectiveOutlooks = & weatherdata . ConvectiveOutlookRun {
Outlooks : [ ] weatherdata . ConvectiveOutlook { categorical , wind } ,
}
ctx . Derived . SPCConvectiveOutlooks = [ ] weatherdata . ConvectiveOutlook { categorical , wind }
ctx . Derived . SPCConvectiveDiscussions = [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 1 , "Mixed risk discussion" , "Only wind is high." , "This discussion should not be emitted without categorical threshold." , "2026-05-29T08:30:00-05:00" ) ,
}
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
if output != nil {
t . Fatalf ( "output = %#v, want mixed day omitted when categorical outlook is below threshold" , output )
}
}
func TestSPCConvectiveDiscussionModuleIncludesOnlyQualifyingDays ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
rank2 := 2
rank4 := 4
ctx := testModuleContext ( )
ctx . Resolved . ValidPeriod . Start = mustParseModuleTime ( "2026-05-30T00:00:00-05:00" )
ctx . Resolved . ValidPeriod . End = mustParseModuleTime ( "2026-05-31T00:00:00-05:00" )
day1Outlook := weatherdata . ConvectiveOutlook {
ID : "day1-marginal" ,
Day : 1 ,
OutlookType : "categorical" ,
Label : "MRGL" ,
LabelText : "Marginal Risk" ,
SeverityRank : & rank2 ,
ValidFrom : mustParseModuleTime ( "2026-05-29T11:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
}
day2Outlook := weatherdata . ConvectiveOutlook {
ID : "day2-enhanced" ,
Day : 2 ,
OutlookType : "categorical" ,
Label : "ENH" ,
LabelText : "Enhanced Risk" ,
SeverityRank : & rank4 ,
ValidFrom : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-31T07:00:00-05:00" ) ,
}
ctx . Collected . SPCConvectiveOutlooks = & weatherdata . ConvectiveOutlookRun {
Outlooks : [ ] weatherdata . ConvectiveOutlook { day1Outlook , day2Outlook } ,
}
ctx . Derived . SPCConvectiveOutlooks = [ ] weatherdata . ConvectiveOutlook { day1Outlook , day2Outlook }
ctx . Derived . SPCConvectiveDiscussions = [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 1 , "Day 1 regional discussion" , "Marginal risk discussion." , "Day 1 text should not be emitted." , "2026-05-29T08:30:00-05:00" ) ,
spcDiscussion ( 2 , "Day 2 regional discussion" , "Enhanced risk discussion." , "Day 2 text should be emitted." , "2026-05-30T08:30:00-05:00" ) ,
}
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
value := moduleValue [ SPCConvectiveDiscussionModule ] ( t , output )
if len ( value . Discussions ) != 1 {
t . Fatalf ( "Discussions = %#v, want only the qualifying day discussion" , value . Discussions )
}
if value . Discussions [ 0 ] . Day != 2 || value . Discussions [ 0 ] . Headline != "Day 2 regional discussion" {
t . Fatalf ( "Discussions[0] = %#v, want day 2 discussion only" , value . Discussions [ 0 ] )
}
if value . Discussions [ 0 ] . PeriodBegins != "2026-05-30 at 7:00 AM" || value . Discussions [ 0 ] . PeriodEnds != "2026-05-31 at 7:00 AM" {
t . Fatalf ( "Period = %q/%q, want qualifying day 2 outlook period" , value . Discussions [ 0 ] . PeriodBegins , value . Discussions [ 0 ] . PeriodEnds )
}
}
func TestSPCConvectiveDiscussionModuleOmitsNonOverlappingQualifyingOutlook ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
rank := 5
ctx := testModuleContext ( )
outlook := weatherdata . ConvectiveOutlook {
ID : "day2-enhanced" ,
Day : 2 ,
OutlookType : "categorical" ,
Label : "ENH" ,
LabelText : "Enhanced Risk" ,
SeverityRank : & rank ,
ValidFrom : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-31T07:00:00-05:00" ) ,
}
ctx . Collected . SPCConvectiveOutlooks = & weatherdata . ConvectiveOutlookRun {
Outlooks : [ ] weatherdata . ConvectiveOutlook { outlook } ,
}
ctx . Derived . SPCConvectiveOutlooks = [ ] weatherdata . ConvectiveOutlook { outlook }
ctx . Derived . SPCConvectiveDiscussions = [ ] weatherdata . ConvectiveOutlookDiscussion {
spcDiscussion ( 2 , "Day 2 regional discussion" , "Enhanced risk discussion." , "Day 2 text should not be emitted for today." , "2026-05-30T08:30:00-05:00" ) ,
}
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
if output != nil {
t . Fatalf ( "output = %#v, want non-overlapping discussion omitted" , output )
}
}
2026-06-12 15:05:42 +00:00
func TestSPCConvectiveDiscussionModuleOmitsMissingDiscussionText ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
ctx := spcConvectiveDiscussionContext ( 3 , [ ] weatherdata . ConvectiveOutlookDiscussion {
{ Day : 1 , Headline : "Severe storms possible" , Summary : "Scattered severe storms are possible." , UpdatedAt : ptrModuleTime ( "2026-05-29T08:30:00-05:00" ) } ,
} )
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
if output != nil {
t . Fatalf ( "output = %#v, want omitted when discussion text is missing" , output )
}
}
func TestSPCConvectiveDiscussionModuleOmitsMissingSource ( t * testing . T ) {
registry := MustDefaultModuleRegistry ( )
ctx := testModuleContext ( )
ctx . Collected . SPCConvectiveOutlooks = nil
ctx . Derived . SPCConvectiveOutlooks = nil
ctx . Derived . SPCConvectiveDiscussions = nil
output , err := registry . BuildModule ( ctx , module . ConfigItem { ID : module . SPCConvectiveDiscussion } )
if err != nil {
t . Fatalf ( "BuildModule() error = %v" , err )
}
if output != nil {
t . Fatalf ( "output = %#v, want omitted for missing optional source" , output )
}
}
func spcConvectiveDiscussionContext ( rank int , discussions [ ] weatherdata . ConvectiveOutlookDiscussion ) ModuleContext {
ctx := testModuleContext ( )
outlook := weatherdata . ConvectiveOutlook {
ID : "day1-categorical" ,
Day : 1 ,
OutlookType : "categorical" ,
Label : "SLGT" ,
LabelText : "Slight Risk" ,
SeverityRank : & rank ,
ValidFrom : mustParseModuleTime ( "2026-05-29T11:00:00-05:00" ) ,
ValidTo : mustParseModuleTime ( "2026-05-30T07:00:00-05:00" ) ,
}
ctx . Collected . SPCConvectiveOutlooks = & weatherdata . ConvectiveOutlookRun {
AsOf : ptrModuleTime ( "2026-05-29T08:00:00-05:00" ) ,
IssuedAt : ptrModuleTime ( "2026-05-29T07:45:00-05:00" ) ,
Outlooks : [ ] weatherdata . ConvectiveOutlook { outlook } ,
}
ctx . Derived . SPCConvectiveOutlooks = [ ] weatherdata . ConvectiveOutlook { outlook }
ctx . Derived . SPCConvectiveDiscussions = discussions
return ctx
}
func spcDiscussion ( day int , headline string , summary string , discussion string , updatedAt string ) weatherdata . ConvectiveOutlookDiscussion {
return weatherdata . ConvectiveOutlookDiscussion {
Day : day ,
Headline : headline ,
Summary : summary ,
Discussion : discussion ,
UpdatedAt : ptrModuleTime ( updatedAt ) ,
}
}
func ptrModuleTime ( value string ) * time . Time {
parsed := mustParseModuleTime ( value )
return & parsed
}