Harden NWS key message parsing

This commit is contained in:
2026-08-03 00:19:48 +00:00
parent 9a09454621
commit 6943a5ebc9
2 changed files with 259 additions and 5 deletions

View File

@@ -740,6 +740,178 @@ func TestParseForecastDiscussionTextSectionKeepsArbitraryDashedProse(t *testing.
}
}
func TestIsForecastDiscussionKeyMessageMetadataLine(t *testing.T) {
tests := []struct {
name string
line string
want bool
}{
{
name: "issued at",
line: "Issued at 300 PM CDT Sat Mar 28 2026",
want: true,
},
{
name: "updated at ASCII case insensitive",
line: "uPdAtEd At 300 PM CDT Sat Mar 28 2026",
want: true,
},
{
name: "label without whitespace boundary",
line: "Updated at300 PM CDT Sat Mar 28 2026",
want: false,
},
{
name: "invalid timestamp",
line: "Updated at not a timestamp",
want: false,
},
{
name: "nonmetadata prose",
line: "Updated atmospheric conditions remain unsettled.",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isForecastDiscussionKeyMessageMetadataLine(tt.line); got != tt.want {
t.Fatalf("isForecastDiscussionKeyMessageMetadataLine(%q) = %t, want %t", tt.line, got, tt.want)
}
})
}
}
func TestStripForecastDiscussionKeyMessageMarker(t *testing.T) {
tests := []struct {
name string
line string
want string
ok bool
}{
{name: "hyphen", line: "- First message.", want: "First message.", ok: true},
{name: "hyphen without whitespace", line: "-First message.", want: "First message.", ok: true},
{name: "asterisk", line: "* First message.", want: "First message.", ok: true},
{name: "numeric parenthesis", line: "1) First message.", want: "First message.", ok: true},
{name: "numeric dot", line: "2. Second message.", want: "Second message.", ok: true},
{name: "composite hyphen", line: "- 1. First message.", want: "First message.", ok: true},
{name: "composite asterisk", line: "* 1) First message.", want: "First message.", ok: true},
{name: "multi digit numeric", line: "12. Twelfth message.", want: "Twelfth message.", ok: true},
{name: "hyphen only", line: "-", want: "", ok: true},
{name: "numeric marker only", line: "1)", want: "", ok: true},
{name: "zero numeric marker", line: "0) Not a marker.", want: "", ok: false},
{name: "overflow numeric marker", line: "999999999999999999999999999999. Too big.", want: "", ok: false},
{name: "alphanumeric numeric prefix", line: "1x Not a marker.", want: "", ok: false},
{name: "numeric marker without boundary", line: "1)Not a marker.", want: "", ok: false},
{name: "numeric marker with unsupported punctuation", line: "1, Not a marker.", want: "", ok: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := stripForecastDiscussionKeyMessageMarker(tt.line)
if ok != tt.ok || got != tt.want {
t.Fatalf("stripForecastDiscussionKeyMessageMarker(%q) = (%q, %t), want (%q, %t)", tt.line, got, ok, tt.want, tt.ok)
}
})
}
}
func TestParseForecastDiscussionKeyMessagesClassifiesListsAndMetadata(t *testing.T) {
tests := []struct {
name string
body []string
want []string
}{
{
name: "numbered list with continuation",
body: []string{
"1) First numbered message.",
"Wrapped continuation.",
"2. Second numbered message.",
},
want: []string{"First numbered message. Wrapped continuation.", "Second numbered message."},
},
{
name: "asterisk list",
body: []string{
"* First asterisk message.",
"*Second asterisk message.",
},
want: []string{"First asterisk message.", "Second asterisk message."},
},
{
name: "unmarked paragraphs",
body: []string{
"First paragraph.",
"Wrapped continuation.",
"",
"",
"Second paragraph.",
},
want: []string{"First paragraph. Wrapped continuation.", "Second paragraph."},
},
{
name: "introductory prose before marked items",
body: []string{
"Introductory prose.",
"",
"- First marked message.",
"Wrapped continuation.",
"- Second marked message.",
},
want: []string{"Introductory prose.", "First marked message. Wrapped continuation.", "Second marked message."},
},
{
name: "leading issued metadata",
body: []string{
"iSsUeD At 300 PM CDT Sat Mar 28 2026",
"- First message.",
},
want: []string{"First message."},
},
{
name: "leading updated metadata",
body: []string{
"Updated at 300 PM CDT Sat Mar 28 2026",
"- First message.",
},
want: []string{"First message."},
},
{
name: "updated atmospheric prose remains content",
body: []string{
"Updated atmospheric conditions remain unsettled.",
"Additional detail.",
},
want: []string{"Updated atmospheric conditions remain unsettled. Additional detail."},
},
{
name: "invalid updated metadata remains content",
body: []string{
"Updated at not a timestamp",
"Additional detail.",
},
want: []string{"Updated at not a timestamp Additional detail."},
},
{
name: "later valid metadata remains content",
body: []string{
"- First message.",
"Updated at 300 PM CDT Sat Mar 28 2026",
},
want: []string{"First message. Updated at 300 PM CDT Sat Mar 28 2026"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseForecastDiscussionKeyMessages(tt.body); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("KeyMessages = %#v, want %#v", got, tt.want)
}
})
}
}
func TestParseForecastDiscussionHTMLParsesCrossOfficeLayout(t *testing.T) {
got, err := ParseForecastDiscussionHTML(loadForecastDiscussionBOUSampleHTML(t))
if err != nil {