|
|
|
|
@@ -152,6 +152,63 @@ func TestBuildDerivedWeekendAndTomorrow(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildDerivedNearTermUsesRollingWindowFacts(t *testing.T) {
|
|
|
|
|
location := testLocation()
|
|
|
|
|
resolved := resolveForTest(t, report.NearTerm, mustParse("2026-05-29T08:30:00-05:00"), location)
|
|
|
|
|
bundle := testBundle(location)
|
|
|
|
|
bundle.Alerts = &weatherdata.AlertRun{Alerts: []json.RawMessage{
|
|
|
|
|
json.RawMessage(`{"event":"Expired Advisory","headline":"Ends at start","severity":"Minor","effective":"2026-05-29T05:00:00-05:00","expires":"2026-05-29T08:30:00-05:00"}`),
|
|
|
|
|
json.RawMessage(`{"event":"Flood Watch","headline":"Flooding possible","severity":"Moderate","effective":"2026-05-29T11:00:00-05:00","expires":"2026-05-29T15:00:00-05:00"}`),
|
|
|
|
|
json.RawMessage(`{"event":"Evening Advisory","headline":"Starts at end","severity":"Minor","effective":"2026-05-29T14:30:00-05:00","expires":"2026-05-29T18:00:00-05:00"}`),
|
|
|
|
|
}}
|
|
|
|
|
bundle.SPCConvectiveOutlooks = testConvectiveOutlookRun(location)
|
|
|
|
|
|
|
|
|
|
derived, err := BuildDerived(BuildDerivedRequest{
|
|
|
|
|
Resolved: resolved,
|
|
|
|
|
Timezone: location.String(),
|
|
|
|
|
Dayparts: testDayparts(),
|
|
|
|
|
Collected: BuildCollected(bundle),
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("BuildDerived() error = %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(derived.ValidPeriodHourlyPeriods) != 4 {
|
|
|
|
|
t.Fatalf("ValidPeriodHourlyPeriods length = %d, want 4 rolling-window hours", len(derived.ValidPeriodHourlyPeriods))
|
|
|
|
|
}
|
|
|
|
|
if derived.ValidPeriodHourlyPeriods[0].StartTime.Format(time.RFC3339) != "2026-05-29T08:00:00-05:00" ||
|
|
|
|
|
derived.ValidPeriodHourlyPeriods[3].StartTime.Format(time.RFC3339) != "2026-05-29T14:00:00-05:00" {
|
|
|
|
|
t.Fatalf("ValidPeriodHourlyPeriods = %#v, want only hours overlapping 8:30 AM-2:30 PM", derived.ValidPeriodHourlyPeriods)
|
|
|
|
|
}
|
|
|
|
|
if len(derived.ValidPeriodNarrativePeriods) != 1 {
|
|
|
|
|
t.Fatalf("ValidPeriodNarrativePeriods length = %d, want overlapping narrative period", len(derived.ValidPeriodNarrativePeriods))
|
|
|
|
|
}
|
|
|
|
|
if len(derived.DailySummaries) != 0 || len(derived.DaypartSummaries) != 0 || derived.StormWindowSummary != nil {
|
|
|
|
|
t.Fatalf("near-term summaries daily=%#v daypart=%#v storm=%#v, want none", derived.DailySummaries, derived.DaypartSummaries, derived.StormWindowSummary)
|
|
|
|
|
}
|
|
|
|
|
if derived.PrecipTiming.FirstPrecipitation == nil || derived.PrecipTiming.FirstPrecipitation.Time.Format(time.RFC3339) != "2026-05-29T08:00:00-05:00" {
|
|
|
|
|
t.Fatalf("PrecipTiming.FirstPrecipitation = %#v, want first selected rainy hour", derived.PrecipTiming.FirstPrecipitation)
|
|
|
|
|
}
|
|
|
|
|
if derived.PrecipTiming.LastPrecipitation == nil || derived.PrecipTiming.LastPrecipitation.Time.Format(time.RFC3339) != "2026-05-29T14:00:00-05:00" {
|
|
|
|
|
t.Fatalf("PrecipTiming.LastPrecipitation = %#v, want final selected rainy window end", derived.PrecipTiming.LastPrecipitation)
|
|
|
|
|
}
|
|
|
|
|
if len(derived.PrecipTiming.PrecipitationWindows) != 2 {
|
|
|
|
|
t.Fatalf("PrecipTiming.PrecipitationWindows = %#v, want two near-term windows", derived.PrecipTiming.PrecipitationWindows)
|
|
|
|
|
}
|
|
|
|
|
if len(derived.AlertOverlaps) != 1 || derived.AlertOverlaps[0].Event != "Flood Watch" {
|
|
|
|
|
t.Fatalf("AlertOverlaps = %#v, want only alert overlapping near-term period", derived.AlertOverlaps)
|
|
|
|
|
}
|
|
|
|
|
if derived.AlertOverlaps[0].Overlap.End.Format(time.RFC3339) != "2026-05-29T14:30:00-05:00" {
|
|
|
|
|
t.Fatalf("Alert overlap end = %s, want clipped to near-term end", derived.AlertOverlaps[0].Overlap.End.Format(time.RFC3339))
|
|
|
|
|
}
|
|
|
|
|
if got, want := outlookIDs(derived.SPCConvectiveOutlooks), []string{"fri-high", "fri-storm", "fri-low", "fri-missing-rank", "fri-probabilistic"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
|
|
|
|
t.Fatalf("SPCConvectiveOutlooks IDs = %#v, want %#v", got, want)
|
|
|
|
|
}
|
|
|
|
|
if got, want := discussionHeadlines(derived.SPCConvectiveDiscussions), []string{"day1 early", "day1 late"}; strings.Join(got, ",") != strings.Join(want, ",") {
|
|
|
|
|
t.Fatalf("SPCConvectiveDiscussions = %#v, want retained discussions for overlapping SPC day %#v", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildDerivedStormBuildsWindowSummary(t *testing.T) {
|
|
|
|
|
location := testLocation()
|
|
|
|
|
resolved := resolveStormForTest(t, location)
|
|
|
|
|
@@ -247,6 +304,22 @@ func TestBuildDerivedSelectsSPCConvectiveOutlooksByValidPeriod(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) {
|
|
|
|
|
location := testLocation()
|
|
|
|
|
resolved := resolveForTest(t, report.NearTerm, mustParse("2026-05-29T08:00:00-05:00"), location)
|
|
|
|
|
resolved.Definition.ID = report.ID("future_report")
|
|
|
|
|
|
|
|
|
|
_, err := BuildDerived(BuildDerivedRequest{
|
|
|
|
|
Resolved: resolved,
|
|
|
|
|
Timezone: location.String(),
|
|
|
|
|
Dayparts: testDayparts(),
|
|
|
|
|
Collected: BuildCollected(testBundle(location)),
|
|
|
|
|
})
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), `derived facts are not implemented for report "future_report"`) {
|
|
|
|
|
t.Fatalf("BuildDerived() error = %v, want actionable unsupported report error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildDerivedSPCConvectiveOutlooksDistinguishesMissingAndCheckedEmpty(t *testing.T) {
|
|
|
|
|
location := testLocation()
|
|
|
|
|
resolved := resolveForTest(t, report.DailyToday, mustParse("2026-05-29T08:00:00-05:00"), location)
|
|
|
|
|
|