Add Tomorrow render context and template

This commit is contained in:
2026-06-14 23:36:47 +00:00
parent 295f06915f
commit ddda42453f
5 changed files with 698 additions and 2 deletions

View File

@@ -114,6 +114,120 @@ func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
}
}
func TestBuildTomorrowRenderContext(t *testing.T) {
metadata := testTomorrowMetadata()
snapshot := testTomorrowSnapshot(t)
generated := Tomorrow{
Summary: "Tomorrow starts quiet, then showers become more likely later in the day.",
ForecastDiscussion: []string{
"Morning conditions should stay mostly dry.",
"Rain chances increase during the afternoon as deeper moisture arrives.",
},
PrecipitationTiming: "The most likely rain window is from midafternoon into early evening.",
}
collected := testCollected()
derived := testTomorrowDerived()
ctx, err := BuildTomorrowRenderContext(metadata, snapshot, generated, collected, derived)
if err != nil {
t.Fatalf("BuildTomorrowRenderContext() error = %v", err)
}
if ctx.Report.Title != "Monday's Weather" {
t.Fatalf("Report.Title = %q, want Monday's Weather", ctx.Report.Title)
}
if ctx.Report.ForecastDateLabel != "Monday, June 15, 2026" || ctx.Report.ForecastDayName != "Monday" {
t.Fatalf("forecast date labels = %q/%q, want Monday labels", ctx.Report.ForecastDateLabel, ctx.Report.ForecastDayName)
}
if ctx.Report.GeneratedAtLabel != "Sunday, June 14, 2026 at 9:14 AM" {
t.Fatalf("Report.GeneratedAtLabel = %q, want friendly generated-at label", ctx.Report.GeneratedAtLabel)
}
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Tomorrow {
t.Fatalf("Modules.Metadata = %#v, want tomorrow metadata", ctx.Modules.Metadata)
}
if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil {
t.Fatalf("current/hourly modules = %#v/%#v, want available in render context", ctx.Modules.CurrentConditions, ctx.Modules.HourlyForecast)
}
if ctx.Modules.DerivedDailySummary == nil || ctx.Modules.DerivedDailySummary.HighTempF == nil || *ctx.Modules.DerivedDailySummary.HighTempF != 74 {
t.Fatalf("Modules.DerivedDailySummary = %#v, want daily summary", ctx.Modules.DerivedDailySummary)
}
if ctx.Modules.DerivedDaypartSummaries == nil || len(*ctx.Modules.DerivedDaypartSummaries) != 2 {
t.Fatalf("Modules.DerivedDaypartSummaries = %#v, want daypart map", ctx.Modules.DerivedDaypartSummaries)
}
if len(ctx.Modules.Dayparts) != 2 || ctx.Modules.Dayparts[0].Key != "morning" || ctx.Modules.Dayparts[1].Key != "afternoon" {
t.Fatalf("Modules.Dayparts = %#v, want configured order", ctx.Modules.Dayparts)
}
if ctx.Modules.PrecipTiming == nil || len(ctx.Modules.PrecipTiming.PrecipitationWindows) != 1 {
t.Fatalf("Modules.PrecipTiming = %#v, want precipitation window", ctx.Modules.PrecipTiming)
}
if ctx.Modules.AlertDigest == nil || ctx.Modules.SPCConvectiveOutlooks == nil || ctx.Modules.AreaForecastDiscussion == nil || ctx.Modules.SPCConvectiveDiscussion == nil || ctx.Modules.WeatherStory == nil || ctx.Modules.TomorrowPlanning == nil {
t.Fatalf("optional modules missing from render context: %#v", ctx.Modules)
}
if !ctx.Collected.FetchedAt.Equal(collected.FetchedAt) {
t.Fatalf("Collected.FetchedAt = %s, want %s", ctx.Collected.FetchedAt, collected.FetchedAt)
}
if len(ctx.Derived.DaypartSummaries) != 2 {
t.Fatalf("Derived.DaypartSummaries = %#v, want passthrough facts", ctx.Derived.DaypartSummaries)
}
rendered, err := reporttemplate.Render("tomorrow", ctx)
if err != nil {
t.Fatalf("Render() error = %v", err)
}
text := string(rendered)
for _, want := range []string{
"# Monday's Weather",
"**Forecast date:** Monday, June 15, 2026",
"Tomorrow starts quiet, then showers become more likely later in the day.",
"- **Morning:** low 60s and partly cloudy.",
"- **Afternoon:** mid 70s and showers. Precipitation chances peak at 70% at 3:00 PM.",
"## Precipitation Timing",
"- **3:00 PM** to **6:00 PM**: Precipitation is expected during this period. The peak precipitation chance is 70% at 3:00 PM.",
"The most likely rain window is from midafternoon into early evening.",
"Morning conditions should stay mostly dry.",
"Rain chances increase during the afternoon as deeper moisture arrives.",
} {
if !strings.Contains(text, want) {
t.Fatalf("rendered template missing %q:\n%s", want, text)
}
}
assertOrderedText(t, text, []string{
"# Monday's Weather",
"## Daypart Forecast",
"- **Morning:**",
"- **Afternoon:**",
"## Precipitation Timing",
"## Forecast Discussion",
})
}
func TestBuildTomorrowRenderContextAllowsOmittedOptionalModules(t *testing.T) {
snapshot, err := module.NewSnapshot(nil)
if err != nil {
t.Fatalf("NewSnapshot() error = %v", err)
}
ctx, err := BuildTomorrowRenderContext(testTomorrowMetadata(), snapshot, Tomorrow{
Summary: "Dry weather is expected tomorrow.",
ForecastDiscussion: []string{"High pressure keeps conditions quiet."},
}, testCollected(), facts.DerivedFacts{})
if err != nil {
t.Fatalf("BuildTomorrowRenderContext() error = %v", err)
}
if ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 {
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
}
rendered, err := reporttemplate.Render("tomorrow", ctx)
if err != nil {
t.Fatalf("Render() error = %v", err)
}
text := string(rendered)
if strings.Contains(text, "## Precipitation Timing") {
t.Fatalf("rendered template included precipitation section without windows:\n%s", text)
}
if !strings.Contains(text, "- No daypart forecast details are available.") {
t.Fatalf("rendered template missing daypart fallback:\n%s", text)
}
}
func testMetadata() briefing.Metadata {
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
return briefing.Metadata{
@@ -134,6 +248,26 @@ func testMetadata() briefing.Metadata {
}
}
func testTomorrowMetadata() briefing.Metadata {
generatedAt := time.Date(2026, 6, 14, 14, 14, 0, 0, time.UTC)
return briefing.Metadata{
RunID: "run-tomorrow",
ReportID: report.Tomorrow,
PromptID: "weather.tomorrow_generated_text",
GeneratedAt: generatedAt,
Units: "imperial",
Timezone: "America/Chicago",
ValidPeriod: timeutil.Period{
Start: time.Date(2026, 6, 15, 5, 0, 0, 0, time.UTC),
End: time.Date(2026, 6, 16, 5, 0, 0, 0, time.UTC),
},
Location: &briefing.LocationContext{
Name: "Brentwood",
Region: "MO",
},
}
}
func testCollected() facts.CollectedFacts {
return facts.CollectedFacts{FetchedAt: time.Date(2026, 5, 29, 13, 31, 0, 0, time.UTC)}
}
@@ -142,6 +276,29 @@ func testDerived() facts.DerivedFacts {
return facts.DerivedFacts{PrecipTiming: forecast.PrecipTiming{ThunderMentioned: true}}
}
func testTomorrowDerived() facts.DerivedFacts {
morningStart := time.Date(2026, 6, 15, 11, 0, 0, 0, time.UTC)
afternoonStart := time.Date(2026, 6, 15, 17, 0, 0, 0, time.UTC)
return facts.DerivedFacts{
DaypartSummaries: []forecast.DaypartSummary{
{
Name: "morning",
Period: timeutil.Period{
Start: morningStart,
End: afternoonStart,
},
},
{
Name: "afternoon",
Period: timeutil.Period{
Start: afternoonStart,
End: time.Date(2026, 6, 15, 23, 0, 0, 0, time.UTC),
},
},
},
}
}
func testSnapshot(t *testing.T) module.Snapshot {
t.Helper()
snapshot, err := module.NewSnapshot([]module.Output{
@@ -273,6 +430,129 @@ func testSnapshot(t *testing.T) module.Snapshot {
return snapshot
}
func testTomorrowSnapshot(t *testing.T) module.Snapshot {
t.Helper()
snapshot, err := module.NewSnapshot([]module.Output{
{
ID: module.Metadata,
StanzaName: string(module.Metadata),
Value: briefing.MetadataModule{
RunID: "run-tomorrow",
ReportID: report.Tomorrow,
PromptID: "weather.tomorrow_generated_text",
GeneratedAt: testTomorrowMetadata().GeneratedAt,
Units: "imperial",
Timezone: "America/Chicago",
ValidPeriod: testTomorrowMetadata().ValidPeriod,
},
},
{
ID: module.CurrentConditions,
StanzaName: string(module.CurrentConditions),
Value: briefing.CurrentConditionsModule{
ConditionTextLower: "clear",
TemperatureF: intPtr(58),
},
},
{
ID: module.HourlyForecast,
StanzaName: string(module.HourlyForecast),
Value: briefing.HourlyForecastModule{
Periods: []briefing.HourlyForecastPeriod{
{HourLabel: "7:00 AM", TextDescriptionLower: "partly cloudy", TemperatureF: floatPtr(63)},
},
},
},
{
ID: module.DerivedDailySummary,
StanzaName: string(module.DerivedDailySummary),
Value: briefing.DerivedDailySummaryModule{
Date: "Monday, June 15, 2026",
HighTempF: intPtr(74),
LowTempF: intPtr(61),
DailyPrecipitationProbability: intPtr(70),
},
},
{
ID: module.DerivedDaypartSummaries,
StanzaName: string(module.DerivedDaypartSummaries),
Value: map[string]briefing.DerivedDaypartSummaryModule{
"afternoon": {
DisplayName: "Afternoon",
TemperaturePhraseF: "mid 70s",
DominantConditionLower: "showers",
MaxPopPercent: intPtr(70),
MaxPopTimeLabel: "3:00 PM",
MentionPrecipitation: true,
},
"morning": {
DisplayName: "Morning",
TemperaturePhraseF: "low 60s",
DominantConditionLower: "partly cloudy",
},
},
},
{
ID: module.PrecipTiming,
StanzaName: string(module.PrecipTiming),
Value: briefing.PrecipTimingModule{
MaxPopPercent: intPtr(70),
MaxPopTime: "3 PM",
PrecipitationWindows: []briefing.PrecipitationWindowModule{
{PeriodBeginsHourLabel: "3:00 PM", PeriodEndsHourLabel: "6:00 PM", MaxPopPercent: intPtr(70), MaxPopHourLabel: "3:00 PM"},
},
},
},
{
ID: module.AlertDigest,
StanzaName: string(module.AlertDigest),
Value: briefing.AlertDigestModule{
Checked: true,
},
},
{
ID: module.SPCConvectiveOutlooks,
StanzaName: string(module.SPCConvectiveOutlooks),
Value: briefing.SPCConvectiveOutlooksModule{
Checked: true,
},
},
{
ID: module.AreaForecastDiscussion,
StanzaName: string(module.AreaForecastDiscussion),
Value: briefing.AreaForecastDiscussionModule{
KeyMessages: []string{"Rain chances increase late."},
},
},
{
ID: module.SPCConvectiveDiscussion,
StanzaName: string(module.SPCConvectiveDiscussion),
Value: briefing.SPCConvectiveDiscussionModule{
IncludedBecause: "convective outlook",
},
},
{
ID: module.WeatherStory,
StanzaName: string(module.WeatherStory),
Value: briefing.WeatherStoryModule{
Available: true,
Title: "Rain returns",
},
},
{
ID: module.TomorrowPlanning,
StanzaName: string(module.TomorrowPlanning),
Value: briefing.TomorrowPlanningModule{
MorningReadiness: []string{"Take sunglasses early."},
},
},
})
if err != nil {
t.Fatalf("NewSnapshot() error = %v", err)
}
return snapshot
}
func floatPtr(value float64) *float64 {
return &value
}
@@ -280,3 +560,18 @@ func floatPtr(value float64) *float64 {
func intPtr(value int) *int {
return &value
}
func assertOrderedText(t *testing.T, text string, ordered []string) {
t.Helper()
previousIndex := -1
for _, want := range ordered {
index := strings.Index(text, want)
if index < 0 {
t.Fatalf("text missing %q:\n%s", want, text)
}
if index <= previousIndex {
t.Fatalf("%q appears out of order in:\n%s", want, text)
}
previousIndex = index
}
}