604 lines
22 KiB
Go
604 lines
22 KiB
Go
package generatedtext
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/reporttemplate"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
|
)
|
|
|
|
func TestBuildHourlyRenderContext(t *testing.T) {
|
|
metadata := testMetadata()
|
|
snapshot := testSnapshot(t)
|
|
generated := Hourly{
|
|
Summary: "Storm chances increase through late morning.",
|
|
ForecastDiscussion: "A front will keep the region unsettled.",
|
|
PrecipitationTiming: "A cold front is moving into the region.",
|
|
Confidence: "Medium confidence in timing.",
|
|
}
|
|
collected := testCollected()
|
|
derived := testDerived()
|
|
ctx, err := BuildHourlyRenderContext(metadata, snapshot, generated, collected, derived)
|
|
if err != nil {
|
|
t.Fatalf("BuildHourlyRenderContext() error = %v", err)
|
|
}
|
|
if ctx.Report.Title != "Hourly Report" {
|
|
t.Fatalf("Report.Title = %q, want Hourly Report", ctx.Report.Title)
|
|
}
|
|
if ctx.Report.LocationName != "Brentwood, MO" {
|
|
t.Fatalf("Report.LocationName = %q, want Brentwood, MO", ctx.Report.LocationName)
|
|
}
|
|
if ctx.Report.ValidPeriodLabel != "2026-05-29 at 8:30 AM to 2026-05-29 at 2:30 PM" {
|
|
t.Fatalf("Report.ValidPeriodLabel = %q, want friendly period", ctx.Report.ValidPeriodLabel)
|
|
}
|
|
if ctx.Report.GeneratedAtLabel != "Friday, May 29, 2026 at 8:30 AM" {
|
|
t.Fatalf("Report.GeneratedAtLabel = %q, want friendly generated-at label", ctx.Report.GeneratedAtLabel)
|
|
}
|
|
if ctx.Modules.CurrentConditions == nil || ctx.Modules.CurrentConditions.ConditionText != "Partly cloudy" || ctx.Modules.CurrentConditions.TemperatureF == nil || *ctx.Modules.CurrentConditions.TemperatureF != 74 {
|
|
t.Fatalf("Modules.CurrentConditions = %#v, want structured current conditions", ctx.Modules.CurrentConditions)
|
|
}
|
|
if ctx.Modules.HourlyForecast == nil || len(ctx.Modules.HourlyForecast.Periods) != 2 {
|
|
t.Fatalf("Modules.HourlyForecast = %#v, want 2 periods", ctx.Modules.HourlyForecast)
|
|
}
|
|
if period := ctx.Modules.HourlyForecast.Periods[1]; period.PeriodBegins != "2026-05-29 at 10:00 AM" || period.TextDescription != "Showers" || period.ProbabilityOfPrecipitationPercent == nil || *period.ProbabilityOfPrecipitationPercent != 70 {
|
|
t.Fatalf("Modules.HourlyForecast.Periods[1] = %#v, want 10 AM showers row", period)
|
|
}
|
|
if ctx.Modules.PrecipTiming == nil || ctx.Modules.PrecipTiming.MaxPopPercent == nil || *ctx.Modules.PrecipTiming.MaxPopPercent != 70 {
|
|
t.Fatalf("Modules.PrecipTiming = %#v, want max pop", ctx.Modules.PrecipTiming)
|
|
}
|
|
if ctx.Modules.AlertDigest == nil || len(ctx.Modules.AlertDigest.Relevant) != 1 || ctx.Modules.AlertDigest.Relevant[0].Event != "Flood Watch" {
|
|
t.Fatalf("Modules.AlertDigest = %#v, want alert", ctx.Modules.AlertDigest)
|
|
}
|
|
if ctx.Modules.SPCConvectiveOutlooks == nil || len(ctx.Modules.SPCConvectiveOutlooks.Outlooks) != 1 || ctx.Modules.SPCConvectiveOutlooks.Outlooks[0].LabelText != "Slight Risk" {
|
|
t.Fatalf("Modules.SPCConvectiveOutlooks = %#v, want outlook", ctx.Modules.SPCConvectiveOutlooks)
|
|
}
|
|
if ctx.Modules.AreaForecastDiscussion == nil || ctx.Modules.AreaForecastDiscussion.ShortTerm != "Short-term discussion favors increasing rain coverage." {
|
|
t.Fatalf("Modules.AreaForecastDiscussion = %#v, want discussion text", ctx.Modules.AreaForecastDiscussion)
|
|
}
|
|
if ctx.Modules.SPCConvectiveDiscussion == nil || len(ctx.Modules.SPCConvectiveDiscussion.Discussions) != 1 || ctx.Modules.SPCConvectiveDiscussion.Discussions[0].Headline != "Mesoscale discussion" {
|
|
t.Fatalf("Modules.SPCConvectiveDiscussion = %#v, want discussion", ctx.Modules.SPCConvectiveDiscussion)
|
|
}
|
|
if ctx.Modules.WeatherStory == nil || ctx.Modules.WeatherStory.Title != "Morning storms" {
|
|
t.Fatalf("Modules.WeatherStory = %#v, want story", ctx.Modules.WeatherStory)
|
|
}
|
|
if !ctx.Collected.FetchedAt.Equal(collected.FetchedAt) {
|
|
t.Fatalf("Collected.FetchedAt = %s, want %s", ctx.Collected.FetchedAt, collected.FetchedAt)
|
|
}
|
|
if !ctx.Derived.PrecipTiming.ThunderMentioned {
|
|
t.Fatalf("Derived.PrecipTiming.ThunderMentioned = false, want true")
|
|
}
|
|
|
|
rendered, err := reporttemplate.Render("hourly", ctx)
|
|
if err != nil {
|
|
t.Fatalf("Render() error = %v", err)
|
|
}
|
|
text := string(rendered)
|
|
for _, want := range []string{
|
|
"# Hourly Report",
|
|
"**Updated:** Friday, May 29, 2026 at 8:30 AM",
|
|
"Storm chances increase through late morning.",
|
|
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%.",
|
|
"- **Flood Watch**: Flood Watch until early afternoon",
|
|
"A cold front is moving into the region.",
|
|
"A front will keep the region unsettled.",
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildHourlyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|
snapshot, err := module.NewSnapshot([]module.Output{
|
|
{ID: module.CurrentConditions, StanzaName: string(module.CurrentConditions), Value: briefing.CurrentConditionsModule{}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
ctx, err := BuildHourlyRenderContext(testMetadata(), snapshot, Hourly{
|
|
Summary: "Storm chances increase.",
|
|
ForecastDiscussion: "A front will keep the region unsettled.",
|
|
}, testCollected(), facts.DerivedFacts{})
|
|
if err != nil {
|
|
t.Fatalf("BuildHourlyRenderContext() error = %v", err)
|
|
}
|
|
if ctx.Modules.CurrentConditions == nil {
|
|
t.Fatal("Modules.CurrentConditions = nil, want populated module")
|
|
}
|
|
if ctx.Modules.HourlyForecast != nil || ctx.Modules.PrecipTiming != nil || ctx.Modules.AlertDigest != nil {
|
|
t.Fatalf("Modules = %#v, want omitted optional modules to remain nil", ctx.Modules)
|
|
}
|
|
}
|
|
|
|
func TestBuildRenderContextReportsModuleExtractionError(t *testing.T) {
|
|
snapshot, err := module.NewSnapshot([]module.Output{
|
|
{ID: module.CurrentConditions, StanzaName: string(module.CurrentConditions), Value: "not a current conditions stanza"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
_, err = BuildHourlyRenderContext(testMetadata(), snapshot, Hourly{
|
|
Summary: "Storm chances increase.",
|
|
ForecastDiscussion: "A front will keep the region unsettled.",
|
|
}, testCollected(), facts.DerivedFacts{})
|
|
if err == nil {
|
|
t.Fatal("BuildHourlyRenderContext() error = nil, want extraction error")
|
|
}
|
|
if !strings.Contains(err.Error(), string(module.CurrentConditions)) {
|
|
t.Fatalf("BuildHourlyRenderContext() error = %v, want module ID", err)
|
|
}
|
|
}
|
|
|
|
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:** Partly cloudy, with temperatures in the low 60s.",
|
|
"- **Afternoon:** Showers, with temperatures in the mid 70s. Chance of precipitation is 70%.",
|
|
"## 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.Metadata != nil || ctx.Modules.CurrentConditions != nil || 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{
|
|
RunID: "run-1",
|
|
ReportID: report.Hourly,
|
|
PromptID: "weather.hourly_generated_text",
|
|
GeneratedAt: generatedAt,
|
|
Units: "imperial",
|
|
Timezone: "America/Chicago",
|
|
ValidPeriod: timeutil.Period{
|
|
Start: generatedAt,
|
|
End: generatedAt.Add(6 * time.Hour),
|
|
},
|
|
Location: &briefing.LocationContext{
|
|
Name: "Brentwood",
|
|
Region: "MO",
|
|
},
|
|
}
|
|
}
|
|
|
|
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)}
|
|
}
|
|
|
|
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{
|
|
{
|
|
ID: module.CurrentConditions,
|
|
StanzaName: string(module.CurrentConditions),
|
|
Value: briefing.CurrentConditionsModule{
|
|
ConditionText: "Partly cloudy",
|
|
ConditionTextLower: "partly cloudy",
|
|
TemperatureF: intPtr(74),
|
|
ApparentTemperatureF: intPtr(76),
|
|
RelativeHumidityPercent: intPtr(71),
|
|
WindSpeedMph: intPtr(8),
|
|
WindDirection: "S",
|
|
WindDirectionText: "south",
|
|
},
|
|
},
|
|
{
|
|
ID: module.HourlyForecast,
|
|
StanzaName: string(module.HourlyForecast),
|
|
Value: briefing.HourlyForecastModule{
|
|
Periods: []briefing.HourlyForecastPeriod{
|
|
{
|
|
HourLabel: "9:00 AM",
|
|
PeriodBegins: "2026-05-29 at 9:00 AM",
|
|
TextDescription: "Cloudy",
|
|
TextDescriptionLower: "cloudy",
|
|
TemperatureF: floatPtr(74),
|
|
ProbabilityOfPrecipitationPercent: floatPtr(30),
|
|
MentionPrecipitation: true,
|
|
WindSpeedMph: floatPtr(8),
|
|
WindDirection: "S",
|
|
},
|
|
{
|
|
HourLabel: "10:00 AM",
|
|
PeriodBegins: "2026-05-29 at 10:00 AM",
|
|
TextDescription: "Showers",
|
|
TextDescriptionLower: "showers",
|
|
TemperatureF: floatPtr(75),
|
|
ProbabilityOfPrecipitationPercent: floatPtr(70),
|
|
MentionPrecipitation: true,
|
|
WindSpeedMph: floatPtr(10),
|
|
WindGustMph: floatPtr(18),
|
|
WindDirection: "S",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: module.PrecipTiming,
|
|
StanzaName: string(module.PrecipTiming),
|
|
Value: briefing.PrecipTimingModule{
|
|
MaxPopPercent: intPtr(70),
|
|
MaxPopTime: "10 AM",
|
|
ProbabilityThreshold: 50,
|
|
PrecipitationWindows: []briefing.PrecipitationWindowModule{
|
|
{
|
|
PeriodBegins: "2026-05-29 at 10:00 AM",
|
|
PeriodBeginsHourLabel: "10:00 AM",
|
|
PeriodEnds: "2026-05-29 at 12:00 PM",
|
|
PeriodEndsHourLabel: "12:00 PM",
|
|
MaxPopPercent: intPtr(70),
|
|
MaxPopTime: "10 AM",
|
|
MaxPopHourLabel: "10:00 AM",
|
|
},
|
|
},
|
|
ThunderMentioned: true,
|
|
},
|
|
},
|
|
{
|
|
ID: module.AlertDigest,
|
|
StanzaName: string(module.AlertDigest),
|
|
Value: briefing.AlertDigestModule{
|
|
Checked: true,
|
|
ActiveCount: 1,
|
|
RelevantCount: 1,
|
|
Relevant: []briefing.AlertSummary{
|
|
{Event: "Flood Watch", Headline: "Flood Watch until early afternoon", Severity: "Moderate"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: module.SPCConvectiveOutlooks,
|
|
StanzaName: string(module.SPCConvectiveOutlooks),
|
|
Value: briefing.SPCConvectiveOutlooksModule{
|
|
Checked: true,
|
|
OutlookCount: 1,
|
|
Outlooks: []briefing.SPCConvectiveOutlookRecord{
|
|
{
|
|
Label: "SLGT",
|
|
LabelText: "Slight Risk",
|
|
PeriodBegins: "2026-05-29 at 7:00 AM",
|
|
PeriodEnds: "2026-05-29 at 3:00 PM",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: module.AreaForecastDiscussion,
|
|
StanzaName: string(module.AreaForecastDiscussion),
|
|
Value: briefing.AreaForecastDiscussionModule{
|
|
KeyMessages: []string{"Storms are most likely late morning."},
|
|
ShortTerm: "Short-term discussion favors increasing rain coverage.",
|
|
},
|
|
},
|
|
{
|
|
ID: module.SPCConvectiveDiscussion,
|
|
StanzaName: string(module.SPCConvectiveDiscussion),
|
|
Value: briefing.SPCConvectiveDiscussionModule{
|
|
IncludedBecause: "categorical severity_rank >= 3",
|
|
Discussions: []briefing.SPCConvectiveDiscussionRecord{
|
|
{Headline: "Mesoscale discussion", Summary: "Strong storms may develop late morning."},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: module.WeatherStory,
|
|
StanzaName: string(module.WeatherStory),
|
|
Value: briefing.WeatherStoryModule{
|
|
Available: true,
|
|
Title: "Morning storms",
|
|
Description: "Morning storms remain the main story.",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
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",
|
|
TemperatureTrend: "steady",
|
|
TemperatureSteadyPhraseF: "mid 70s",
|
|
DominantConditionDisplay: "Showers",
|
|
DominantConditionLower: "showers",
|
|
MaxPopPercent: intPtr(70),
|
|
MaxPopTimeLabel: "3:00 PM",
|
|
MentionPrecipitation: true,
|
|
},
|
|
"morning": {
|
|
DisplayName: "Morning",
|
|
TemperatureTrend: "steady",
|
|
TemperatureSteadyPhraseF: "low 60s",
|
|
DominantConditionDisplay: "Partly cloudy",
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|