From 55f01805993310d05cd004bef7bc402f9569c354 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 12 Jun 2026 17:31:37 +0000 Subject: [PATCH] Add near-term module composition --- internal/briefing/base_modules_test.go | 43 ++++++++++++++++++++++++++ internal/briefing/modules.go | 4 +-- internal/briefing/modules_test.go | 42 +++++++++++++++++++++++-- internal/report/near_term_report.go | 17 +++++++++- internal/report/period_test.go | 14 +++++++-- 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/internal/briefing/base_modules_test.go b/internal/briefing/base_modules_test.go index 3d63ad8..e51a877 100644 --- a/internal/briefing/base_modules_test.go +++ b/internal/briefing/base_modules_test.go @@ -97,6 +97,21 @@ func TestHourlyForecastModuleRejectsUnsupportedReports(t *testing.T) { } } +func TestHourlyForecastModuleBuildsForNearTerm(t *testing.T) { + registry := MustDefaultModuleRegistry() + ctx := testModuleContext() + ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.NearTerm) + + output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.HourlyForecast}) + if err != nil { + t.Fatalf("BuildModule() error = %v", err) + } + value := moduleValue[HourlyForecastModule](t, output) + if len(value.Periods) != 1 || value.Periods[0].TextDescription != "Showers likely." { + t.Fatalf("HourlyForecast = %#v, want near-term hourly period", value) + } +} + func TestNarrativeForecastModuleUsesValidPeriodNarrativePeriods(t *testing.T) { registry := MustDefaultModuleRegistry() ctx := testModuleContext() @@ -320,6 +335,34 @@ func TestAreaForecastDiscussionModuleCanSelectSections(t *testing.T) { } } +func TestAreaForecastDiscussionModuleUsesNearTermDefaultSections(t *testing.T) { + registry := MustDefaultModuleRegistry() + ctx := testModuleContext() + ctx.Resolved.Definition = report.DefaultRegistry().MustLookup(report.NearTerm) + var item module.ConfigItem + for _, candidate := range ctx.Resolved.Definition.Modules { + if candidate.ID == module.AreaForecastDiscussion { + item = candidate + break + } + } + if item.ID == "" { + t.Fatal("near-term default modules missing area_forecast_discussion") + } + + output, err := registry.BuildModule(ctx, item) + if err != nil { + t.Fatalf("BuildModule() error = %v", err) + } + afd := moduleValue[AreaForecastDiscussionModule](t, output) + if len(afd.KeyMessages) != 1 || afd.ShortTerm != "Showers increase this afternoon." { + t.Fatalf("AFD = %#v, want key messages and short term", afd) + } + if afd.Product != "" || afd.LongTerm != "" { + t.Fatalf("AFD = %#v, want product and long term omitted", afd) + } +} + func testModuleContext() ModuleContext { generatedAt := mustParseModuleTime("2026-05-29T08:00:00-05:00") definition := report.DefaultRegistry().MustLookup(report.DailyToday) diff --git a/internal/briefing/modules.go b/internal/briefing/modules.go index 3d543fd..2fb612b 100644 --- a/internal/briefing/modules.go +++ b/internal/briefing/modules.go @@ -249,7 +249,7 @@ func (d ModuleDefinition) ValidateOptions(options any) error { } func defaultModuleDefinitions() []ModuleDefinition { - allReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.ThreeDay, report.Weekend, report.Storm} + allReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.NearTerm, report.ThreeDay, report.Weekend, report.Storm} daypartReports := []report.ID{report.DailyToday, report.DailyTomorrow, report.ThreeDay, report.Weekend} return []ModuleDefinition{ { @@ -286,7 +286,7 @@ func defaultModuleDefinitions() []ModuleDefinition { DefaultOptions: module.HourlyForecastOptions{}, RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast}, RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods}, - SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow}, + SupportedReports: []report.ID{report.DailyToday, report.DailyTomorrow, report.NearTerm}, MissingData: module.MissingDataOmit, Builder: buildHourlyForecastModule, }, diff --git a/internal/briefing/modules_test.go b/internal/briefing/modules_test.go index aa418ed..5648eac 100644 --- a/internal/briefing/modules_test.go +++ b/internal/briefing/modules_test.go @@ -32,9 +32,6 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) { registry := MustDefaultModuleRegistry() for _, definition := range report.DefaultRegistry().All() { t.Run(string(definition.ID), func(t *testing.T) { - if len(definition.Modules) == 0 { - return - } ctx := derivedModuleContext(definition.ID) var outputs []module.Output for _, item := range definition.Modules { @@ -57,6 +54,27 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) { } } +func TestNearTermDefaultModuleOptions(t *testing.T) { + definition := report.DefaultRegistry().MustLookup(report.NearTerm) + var found bool + for _, item := range definition.Modules { + if item.ID != module.AreaForecastDiscussion { + continue + } + found = true + options, ok := item.Options.(module.AreaForecastDiscussionOptions) + if !ok { + t.Fatalf("AFD options type = %T, want AreaForecastDiscussionOptions", item.Options) + } + if strings.Join(options.Sections, ",") != "key_messages,short_term" { + t.Fatalf("AFD sections = %#v, want key messages and short term", options.Sections) + } + } + if !found { + t.Fatal("near-term default modules missing area_forecast_discussion") + } +} + func TestModuleRegistryRejectsUnknownModule(t *testing.T) { registry := MustDefaultModuleRegistry() err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.ID("unknown")}}) @@ -94,6 +112,24 @@ func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) { } } +func TestModuleRegistryRejectsNearTermIncompatibleModules(t *testing.T) { + registry := MustDefaultModuleRegistry() + for _, id := range []module.ID{ + module.NarrativeForecast, + module.DerivedDailySummary, + module.DerivedDaypartSummaries, + module.OutdoorWindows, + module.TomorrowPlanning, + } { + t.Run(string(id), func(t *testing.T) { + err := registry.ValidateComposition(report.NearTerm, []module.ConfigItem{{ID: id}}) + if err == nil || !strings.Contains(err.Error(), `not compatible with report "near_term"`) { + t.Fatalf("ValidateComposition() error = %v, want incompatible near-term module", err) + } + }) + } +} + func TestModuleRegistryRejectsDefinitionsWithoutBuilders(t *testing.T) { _, err := NewModuleRegistry([]ModuleDefinition{ {ID: module.Metadata, StanzaName: "metadata", DefaultOptions: module.MetadataOptions{}}, diff --git a/internal/report/near_term_report.go b/internal/report/near_term_report.go index 275df67..4a81479 100644 --- a/internal/report/near_term_report.go +++ b/internal/report/near_term_report.go @@ -25,7 +25,22 @@ func nearTermDefinition() Definition { } func nearTermModules() []module.ConfigItem { - return nil + return []module.ConfigItem{ + {ID: module.Metadata}, + {ID: module.CurrentConditions}, + {ID: module.HourlyForecast}, + {ID: module.PrecipTiming}, + {ID: module.AlertDigest}, + {ID: module.SPCConvectiveOutlooks}, + { + ID: module.AreaForecastDiscussion, + Options: module.AreaForecastDiscussionOptions{ + Sections: []string{"key_messages", "short_term"}, + }, + }, + {ID: module.SPCConvectiveDiscussion}, + {ID: module.WeatherStory}, + } } func resolveNearTerm(req ResolveRequest) (timeutil.Period, error) { diff --git a/internal/report/period_test.go b/internal/report/period_test.go index fd4d7c8..8b388a8 100644 --- a/internal/report/period_test.go +++ b/internal/report/period_test.go @@ -384,8 +384,18 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) { }, }, { - id: NearTerm, - want: []module.ID{}, + id: NearTerm, + want: []module.ID{ + module.Metadata, + module.CurrentConditions, + module.HourlyForecast, + module.PrecipTiming, + module.AlertDigest, + module.SPCConvectiveOutlooks, + module.AreaForecastDiscussion, + module.SPCConvectiveDiscussion, + module.WeatherStory, + }, }, { id: ThreeDay,