From fa1b2cb3902368bd64ef2806193fe139339da01f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 12 Jun 2026 12:58:28 +0000 Subject: [PATCH] Filter outlook discussions with retained outlooks --- internal/app/service.go | 32 +++++++++++++++ internal/app/service_test.go | 76 +++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/internal/app/service.go b/internal/app/service.go index 416e850..ee11273 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -85,6 +85,7 @@ func (s *Service) LatestConvectiveOutlook(ctx context.Context, filter OutlookFil } } out.Outlooks = outlooks + out.Discussions = filterOutlookDiscussions(out.Discussions, out.Outlooks) return out, nil } @@ -123,6 +124,12 @@ func cloneOutlookRun(run *model.WeatherOutlookRun) *model.WeatherOutlookRun { out.Outlooks[i] = cloneOutlook(run.Outlooks[i]) } } + if run.Discussions != nil { + out.Discussions = make([]model.WeatherOutlookDiscussion, len(run.Discussions)) + for i := range run.Discussions { + out.Discussions[i] = cloneOutlookDiscussion(run.Discussions[i]) + } + } return &out } @@ -135,6 +142,31 @@ func cloneOutlook(outlook model.WeatherOutlook) model.WeatherOutlook { return out } +func cloneOutlookDiscussion(discussion model.WeatherOutlookDiscussion) model.WeatherOutlookDiscussion { + out := discussion + out.UpdatedAt = copyTime(discussion.UpdatedAt) + return out +} + +func filterOutlookDiscussions(discussions []model.WeatherOutlookDiscussion, outlooks []model.WeatherOutlook) []model.WeatherOutlookDiscussion { + if len(outlooks) == 0 { + return []model.WeatherOutlookDiscussion{} + } + + retainedDays := make(map[int]struct{}, len(outlooks)) + for _, outlook := range outlooks { + retainedDays[outlook.Day] = struct{}{} + } + + out := discussions[:0] + for _, discussion := range discussions { + if _, ok := retainedDays[discussion.Day]; ok { + out = append(out, discussion) + } + } + return out +} + func copyFloat64(value *float64) *float64 { if value == nil { return nil diff --git a/internal/app/service_test.go b/internal/app/service_test.go index c7b2102..f767eab 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -5,6 +5,7 @@ package app import ( "context" "errors" + "strconv" "testing" "time" @@ -131,6 +132,7 @@ func TestServiceDelegatesLatestConvectiveOutlookRun(t *testing.T) { if run == nil || run.LocationID != "stl" { t.Fatalf("unexpected outlook run: %+v", run) } + assertDiscussionDays(t, run, []int{1, 2}) } func TestServiceLatestConvectiveOutlookNoData(t *testing.T) { @@ -208,6 +210,7 @@ func TestServiceLatestConvectiveOutlookFiltersByDay(t *testing.T) { t.Fatalf("unexpected error: %v", err) } assertOutlookIDs(t, run, []string{"day-2"}) + assertDiscussionDays(t, run, []int{2}) } func TestServiceLatestConvectiveOutlookFiltersByOutlookType(t *testing.T) { @@ -219,6 +222,7 @@ func TestServiceLatestConvectiveOutlookFiltersByOutlookType(t *testing.T) { t.Fatalf("unexpected error: %v", err) } assertOutlookIDs(t, run, []string{"tor-1"}) + assertDiscussionDays(t, run, []int{1}) } func TestServiceLatestConvectiveOutlookFiltersByContainsLocation(t *testing.T) { @@ -230,7 +234,21 @@ func TestServiceLatestConvectiveOutlookFiltersByContainsLocation(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - assertOutlookIDs(t, run, []string{"cat-1", "tor-1"}) + assertOutlookIDs(t, run, []string{"cat-1", "tor-1", "day-2"}) + assertDiscussionDays(t, run, []int{1, 2}) +} + +func TestServiceLatestConvectiveOutlookContainsLocationFalseReturnsEmptyRun(t *testing.T) { + containsLocation := false + repo := &fakeRepository{outlookRun: testOutlookRun()} + svc := NewService(repo) + + run, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{ContainsLocation: &containsLocation}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertOutlookIDs(t, run, nil) + assertDiscussionDays(t, run, nil) } func TestServiceLatestConvectiveOutlookFiltersByActiveAt(t *testing.T) { @@ -243,6 +261,7 @@ func TestServiceLatestConvectiveOutlookFiltersByActiveAt(t *testing.T) { t.Fatalf("unexpected error: %v", err) } assertOutlookIDs(t, run, []string{"cat-1", "tor-1"}) + assertDiscussionDays(t, run, []int{1}) } func TestServiceLatestConvectiveOutlookCombinesFilters(t *testing.T) { @@ -262,6 +281,7 @@ func TestServiceLatestConvectiveOutlookCombinesFilters(t *testing.T) { t.Fatalf("unexpected error: %v", err) } assertOutlookIDs(t, run, []string{"cat-1"}) + assertDiscussionDays(t, run, []int{1}) } func TestServiceLatestConvectiveOutlookActiveAtBoundary(t *testing.T) { @@ -277,12 +297,14 @@ func TestServiceLatestConvectiveOutlookActiveAtBoundary(t *testing.T) { t.Fatalf("unexpected validFrom error: %v", err) } assertOutlookIDs(t, fromRun, []string{"cat-1"}) + assertDiscussionDays(t, fromRun, []int{1}) toRun, err := svc.LatestConvectiveOutlook(context.Background(), OutlookFilter{ActiveAt: &validTo}) if err != nil { t.Fatalf("unexpected validTo error: %v", err) } assertOutlookIDs(t, toRun, nil) + assertDiscussionDays(t, toRun, nil) } func TestServiceLatestConvectiveOutlookKeepsRunMetadataWithEmptyOutlooks(t *testing.T) { @@ -306,6 +328,12 @@ func TestServiceLatestConvectiveOutlookKeepsRunMetadataWithEmptyOutlooks(t *test if len(run.Outlooks) != 0 { t.Fatalf("expected no outlooks, got %+v", run.Outlooks) } + if run.Discussions == nil { + t.Fatal("expected empty discussions slice, got nil") + } + if len(run.Discussions) != 0 { + t.Fatalf("expected no discussions, got %+v", run.Discussions) + } } func TestServiceLatestConvectiveOutlookDoesNotMutateRepositoryRun(t *testing.T) { @@ -325,9 +353,12 @@ func TestServiceLatestConvectiveOutlookDoesNotMutateRepositoryRun(t *testing.T) *run.Longitude = -99 *run.IssuedAt = testTime(99) *run.Outlooks[0].SeverityRank = 99 + *run.Discussions[0].UpdatedAt = testTime(98) run.Outlooks[0].Geometry[0] = '{' run.Outlooks[0].ID = "changed" + run.Discussions[0].Headline = "changed" run.Outlooks = run.Outlooks[:1] + run.Discussions = run.Discussions[:1] if *original.Latitude != 38.62 { t.Fatalf("expected original latitude unchanged, got %v", *original.Latitude) @@ -350,6 +381,15 @@ func TestServiceLatestConvectiveOutlookDoesNotMutateRepositoryRun(t *testing.T) if len(original.Outlooks) != 3 { t.Fatalf("expected original outlook slice unchanged, got %d entries", len(original.Outlooks)) } + if original.Discussions[0].UpdatedAt == nil || !original.Discussions[0].UpdatedAt.Equal(testTime(10)) { + t.Fatalf("expected original discussion updatedAt unchanged, got %v", original.Discussions[0].UpdatedAt) + } + if original.Discussions[0].Headline != "Day 1 headline" { + t.Fatalf("expected original discussion headline unchanged, got %q", original.Discussions[0].Headline) + } + if len(original.Discussions) != 3 { + t.Fatalf("expected original discussion slice unchanged, got %d entries", len(original.Discussions)) + } } func TestServicePropagatesErrors(t *testing.T) { @@ -376,8 +416,25 @@ func testOutlookRun() *model.WeatherOutlookRun { Outlooks: []model.WeatherOutlook{ testOutlook("cat-1", 1, "categorical", true, testTime(12), testTime(18), 5, `["cat"]`), testOutlook("tor-1", 1, "tornado", true, testTime(13), testTime(19), 7, `["tor"]`), - testOutlook("day-2", 2, "wind", false, testTime(18), testTime(24), 2, `["wind"]`), + testOutlook("day-2", 2, "wind", true, testTime(18), testTime(24), 2, `["wind"]`), }, + Discussions: []model.WeatherOutlookDiscussion{ + testOutlookDiscussion(1), + testOutlookDiscussion(2), + testOutlookDiscussion(3), + }, + } +} + +func testOutlookDiscussion(day int) model.WeatherOutlookDiscussion { + updatedAt := testTime(9 + day) + dayText := strconv.Itoa(day) + return model.WeatherOutlookDiscussion{ + Day: day, + Headline: "Day " + dayText + " headline", + Summary: "Day " + dayText + " summary", + Discussion: "Day " + dayText + " discussion", + UpdatedAt: &updatedAt, } } @@ -421,3 +478,18 @@ func assertOutlookIDs(t *testing.T, run *model.WeatherOutlookRun, want []string) } } } + +func assertDiscussionDays(t *testing.T, run *model.WeatherOutlookRun, want []int) { + t.Helper() + if run == nil { + t.Fatal("expected outlook run") + } + if len(run.Discussions) != len(want) { + t.Fatalf("expected discussion days %v, got %+v", want, run.Discussions) + } + for i := range want { + if run.Discussions[i].Day != want[i] { + t.Fatalf("expected discussion days %v, got %+v", want, run.Discussions) + } + } +}