diff --git a/internal/providers/nws/forecast_discussion.go b/internal/providers/nws/forecast_discussion.go index 8a026a5..7ffc511 100644 --- a/internal/providers/nws/forecast_discussion.go +++ b/internal/providers/nws/forecast_discussion.go @@ -48,6 +48,7 @@ type forecastDiscussionSectionBlock struct { var ( forecastDiscussionSectionRoles = map[string]forecastDiscussionSectionRole{ "KEY MESSAGES": forecastDiscussionSectionRoleKeyMessages, + "KEY POINTS": forecastDiscussionSectionRoleKeyMessages, "SHORT TERM": forecastDiscussionSectionRoleShortTerm, "LONG TERM": forecastDiscussionSectionRoleLongTerm, } diff --git a/internal/providers/nws/forecast_discussion_test.go b/internal/providers/nws/forecast_discussion_test.go index 9f96c6d..c2519ce 100644 --- a/internal/providers/nws/forecast_discussion_test.go +++ b/internal/providers/nws/forecast_discussion_test.go @@ -279,11 +279,29 @@ func TestParseForecastDiscussionSectionHeading(t *testing.T) { } func TestForecastDiscussionSectionRoleHeadingsParse(t *testing.T) { - if len(forecastDiscussionSectionRoles) != 3 { - t.Fatalf("role registry has %d entries, want 3", len(forecastDiscussionSectionRoles)) + if len(forecastDiscussionSectionRoles) != 4 { + t.Fatalf("role registry has %d entries, want 4", len(forecastDiscussionSectionRoles)) } - if _, ok := forecastDiscussionSectionRoles["AVIATION"]; ok { - t.Fatalf("AVIATION must remain boundary-only") + + for _, tt := range []struct { + section string + role forecastDiscussionSectionRole + }{ + {section: "KEY MESSAGES", role: forecastDiscussionSectionRoleKeyMessages}, + {section: "KEY POINTS", role: forecastDiscussionSectionRoleKeyMessages}, + {section: "SHORT TERM", role: forecastDiscussionSectionRoleShortTerm}, + {section: "LONG TERM", role: forecastDiscussionSectionRoleLongTerm}, + } { + role, ok := forecastDiscussionSectionRoles[tt.section] + if !ok || role != tt.role { + t.Fatalf("role for %q = %d, present = %t; want %d", tt.section, role, ok, tt.role) + } + } + + for _, section := range []string{"NEAR TERM", "DISCUSSION", "AVIATION"} { + if _, ok := forecastDiscussionSectionRoles[section]; ok { + t.Fatalf("%s must remain boundary-only", section) + } } for section := range forecastDiscussionSectionRoles { @@ -464,6 +482,90 @@ func TestParseForecastDiscussionTextKeepsFirstMappedSections(t *testing.T) { } } +func TestParseForecastDiscussionTextKeepsFirstKeyMessageRole(t *testing.T) { + tests := []struct { + name string + lines []string + want []string + }{ + { + name: "key points only", + lines: []string{ + ".KEY POINTS...", + "- First key point.", + }, + want: []string{"First key point."}, + }, + { + name: "key points before key messages", + lines: []string{ + ".KEY POINTS...", + "- First key point.", + "&&", + ".KEY MESSAGES...", + "- Later key message.", + }, + want: []string{"First key point."}, + }, + { + name: "key messages before key points", + lines: []string{ + ".KEY MESSAGES...", + "- First key message.", + "&&", + ".KEY POINTS...", + "- Later key point.", + }, + want: []string{"First key message."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + text := strings.Join(append([]string{ + "AFDLSX", + "National Weather Service Saint Louis MO", + "224 PM CDT Sat Mar 28 2026", + "", + }, tt.lines...), "\n") + + got, err := ParseForecastDiscussionText(text) + if err != nil { + t.Fatalf("ParseForecastDiscussionText() error = %v", err) + } + if !reflect.DeepEqual(got.KeyMessages, tt.want) { + t.Fatalf("KeyMessages = %#v, want %#v", got.KeyMessages, tt.want) + } + }) + } +} + +func TestParseForecastDiscussionTextKeepsUnmappedSectionsBoundaryOnly(t *testing.T) { + text := strings.Join([]string{ + "AFDLSX", + "National Weather Service Saint Louis MO", + "224 PM CDT Sat Mar 28 2026", + "", + ".SHORT TERM... (Tonight)", + "Short-term prose.", + ".NEAR TERM... (This Evening)", + "Near-term prose.", + ".LONG TERM... (Tomorrow)", + "Long-term prose.", + }, "\n") + + got, err := ParseForecastDiscussionText(text) + if err != nil { + t.Fatalf("ParseForecastDiscussionText() error = %v", err) + } + if got.ShortTerm == nil || got.ShortTerm.Text != "Short-term prose." { + t.Fatalf("ShortTerm = %#v, want only short-term prose", got.ShortTerm) + } + if got.LongTerm == nil || got.LongTerm.Text != "Long-term prose." { + t.Fatalf("LongTerm = %#v, want only long-term prose", got.LongTerm) + } +} + func TestParseForecastDiscussionTextSectionKeepsHeadingQualifiers(t *testing.T) { tests := []struct { name string