318 lines
13 KiB
Go
318 lines
13 KiB
Go
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)
|
|
if value.IncludedBecause != "categorical severity_rank >= 3" || len(value.Discussions) != 1 {
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
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)
|
|
for _, field := range []string{"included_because", "discussions", "period_begins", "period_ends", "headline", "summary", "discussion", "updated_at"} {
|
|
if !strings.Contains(text, field) {
|
|
t.Fatalf("json = %s, want field %s", text, field)
|
|
}
|
|
}
|
|
if strings.Contains(text, `"period":`) {
|
|
t.Fatalf("json = %s, want period_begins/period_ends instead of period", text)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|