1427 lines
55 KiB
Go
1427 lines
55 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.",
|
|
}
|
|
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.CurrentConditions.ConditionTextLower != "partly cloudy" || ctx.Modules.CurrentConditions.WindDirectionText != "south" {
|
|
t.Fatalf("Modules.CurrentConditions helpers = %#v, want rich template helper fields", 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 period := ctx.Modules.HourlyForecast.Periods[1]; period.HourLabel != "10:00 AM" || period.TextDescriptionLower != "showers" || !period.MentionPrecipitation {
|
|
t.Fatalf("Modules.HourlyForecast.Periods[1] helpers = %#v, want rich template helper fields", 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 in effect from May 29 at 10:00 AM to May 29 at 2:30 PM.",
|
|
"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)
|
|
}
|
|
}
|
|
if strings.Contains(text, "Avoid low-water crossings.") {
|
|
t.Fatalf("rendered template includes alert instruction:\n%s", 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 TestBuildTodayRenderContext(t *testing.T) {
|
|
metadata := testTodayMetadata()
|
|
snapshot := testTodaySnapshot(t)
|
|
generated := Today{
|
|
Summary: "Today 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 := testTodayDerived()
|
|
|
|
ctx, err := BuildTodayRenderContext(metadata, snapshot, generated, collected, derived)
|
|
if err != nil {
|
|
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
|
}
|
|
if ctx.Report.Title != "Today's Weather" {
|
|
t.Fatalf("Report.Title = %q, want Today'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 != "Monday, June 15, 2026 at 7: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.Today {
|
|
t.Fatalf("Modules.Metadata = %#v, want today 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 morning := (*ctx.Modules.DerivedDaypartSummaries)["morning"]; morning.DominantConditionDisplay == "" || morning.DominantConditionLower == "" || morning.TemperatureSteadyPhraseF == "" {
|
|
t.Fatalf("today rich morning daypart helpers = %#v, want render-context helper fields", morning)
|
|
}
|
|
if afternoon := (*ctx.Modules.DerivedDaypartSummaries)["afternoon"]; afternoon.MaxPopTimeLabel != "3:00 PM" {
|
|
t.Fatalf("today rich afternoon daypart = %#v, want max pop time label helper", afternoon)
|
|
}
|
|
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.TodayPlanning == nil {
|
|
t.Fatalf("optional modules missing from render context: %#v", ctx.Modules)
|
|
}
|
|
if ctx.Modules.TodayPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
|
t.Fatalf("Modules.TodayPlanning = %#v, want today planning facts", ctx.Modules.TodayPlanning)
|
|
}
|
|
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("today", ctx)
|
|
if err != nil {
|
|
t.Fatalf("Render() error = %v", err)
|
|
}
|
|
text := string(rendered)
|
|
for _, want := range []string{
|
|
"# Today's Weather",
|
|
"**Forecast date:** Monday, June 15, 2026",
|
|
"Today starts quiet, then showers become more likely later in the day.",
|
|
"Currently, it is 58°F and clear. It feels like 57°F, with a relative humidity of 61% and winds from the northwest at 9 mph.",
|
|
"- **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**: Expect showers. 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{
|
|
"# Today's Weather",
|
|
"## Current Conditions",
|
|
"## Daypart Forecast",
|
|
"- **Morning:**",
|
|
"- **Afternoon:**",
|
|
"## Precipitation Timing",
|
|
"## Forecast Discussion",
|
|
})
|
|
if strings.Contains(text, "## Planning Notes") || strings.Contains(text, "- Take sunglasses early.") || strings.Contains(text, "Forecast details are limited") {
|
|
t.Fatalf("rendered template included removed Today template content:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestTodayRenderContextPreservesUnicodeDaypartDisplayNames(t *testing.T) {
|
|
metadata := testTodayMetadata()
|
|
snapshot := testTodaySnapshot(t)
|
|
for i := range snapshot.Outputs {
|
|
if snapshot.Outputs[i].ID != module.DerivedDaypartSummaries {
|
|
continue
|
|
}
|
|
dayparts := snapshot.Outputs[i].Value.(map[string]briefing.DerivedDaypartSummaryModule)
|
|
morning := dayparts["morning"]
|
|
morning.DisplayName = "Mañana"
|
|
dayparts["morning"] = morning
|
|
}
|
|
|
|
ctx, err := BuildTodayRenderContext(metadata, snapshot, Today{
|
|
Summary: "Today summary.",
|
|
ForecastDiscussion: []string{"Morning conditions stay quiet."},
|
|
}, testCollected(), testTodayDerived())
|
|
if err != nil {
|
|
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
|
}
|
|
rendered, err := reporttemplate.Render("today", ctx)
|
|
if err != nil {
|
|
t.Fatalf("Render() error = %v", err)
|
|
}
|
|
if !strings.Contains(string(rendered), "Mañana") {
|
|
t.Fatalf("rendered report = %q, want Unicode daypart display name", rendered)
|
|
}
|
|
}
|
|
|
|
func TestBuildTodayRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|
snapshot, err := module.NewSnapshot(nil)
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
ctx, err := BuildTodayRenderContext(testTodayMetadata(), snapshot, Today{
|
|
Summary: "Dry weather is expected today.",
|
|
ForecastDiscussion: []string{"High pressure keeps conditions quiet."},
|
|
}, testCollected(), facts.DerivedFacts{})
|
|
if err != nil {
|
|
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
|
}
|
|
if ctx.Modules.Metadata != nil || ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || len(ctx.Modules.Dayparts) != 0 || ctx.Modules.TodayPlanning != nil {
|
|
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
|
}
|
|
rendered, err := reporttemplate.Render("today", ctx)
|
|
if err != nil {
|
|
t.Fatalf("Render() error = %v", err)
|
|
}
|
|
text := string(rendered)
|
|
for _, unwanted := range []string{"## Current Conditions", "## Precipitation Timing", "## Planning Notes", "Forecast details are limited"} {
|
|
if strings.Contains(text, unwanted) {
|
|
t.Fatalf("rendered template included %q without module data:\n%s", unwanted, text)
|
|
}
|
|
}
|
|
if !strings.Contains(text, "- No daypart forecast details are available.") {
|
|
t.Fatalf("rendered template missing daypart fallback:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestBuildDayStyleRenderContextsPopulateSharedFieldsAndPlanningModules(t *testing.T) {
|
|
type result struct {
|
|
title string
|
|
forecastDateLabel string
|
|
generatedAtLabel string
|
|
timezone string
|
|
validPeriod timeutil.Period
|
|
metadata bool
|
|
current bool
|
|
hourly bool
|
|
dailySummary bool
|
|
daypartSummaries bool
|
|
dayparts int
|
|
precip bool
|
|
alerts bool
|
|
outlooks bool
|
|
discussion bool
|
|
spcDiscussion bool
|
|
story bool
|
|
planning bool
|
|
collected bool
|
|
derived bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
wantTitle string
|
|
wantGeneratedLabel string
|
|
wantDayparts int
|
|
build func(t *testing.T) result
|
|
}{
|
|
{
|
|
name: "daily",
|
|
wantTitle: "Monday's Weather",
|
|
wantGeneratedLabel: "Saturday, June 13, 2026 at 9:14 AM",
|
|
wantDayparts: 3,
|
|
build: func(t *testing.T) result {
|
|
t.Helper()
|
|
collected := testCollected()
|
|
derived := testDailyDerived()
|
|
ctx, err := BuildDailyRenderContext(testDailyMetadata(), testDailySnapshot(t), Daily{
|
|
Summary: "Daily summary.",
|
|
ForecastDiscussion: []string{"Daily discussion."},
|
|
}, collected, derived)
|
|
if err != nil {
|
|
t.Fatalf("BuildDailyRenderContext() error = %v", err)
|
|
}
|
|
return result{
|
|
title: ctx.Report.Title,
|
|
forecastDateLabel: ctx.Report.ForecastDateLabel,
|
|
generatedAtLabel: ctx.Report.GeneratedAtLabel,
|
|
timezone: ctx.Report.Timezone,
|
|
validPeriod: ctx.Report.ValidPeriod,
|
|
metadata: ctx.Modules.Metadata != nil && ctx.Modules.Metadata.ReportID == report.Daily,
|
|
current: ctx.Modules.CurrentConditions != nil,
|
|
hourly: ctx.Modules.HourlyForecast != nil,
|
|
dailySummary: ctx.Modules.DerivedDailySummary != nil,
|
|
daypartSummaries: ctx.Modules.DerivedDaypartSummaries != nil,
|
|
dayparts: len(ctx.Modules.Dayparts),
|
|
precip: ctx.Modules.PrecipTiming != nil,
|
|
alerts: ctx.Modules.AlertDigest != nil,
|
|
outlooks: ctx.Modules.SPCConvectiveOutlooks != nil,
|
|
discussion: ctx.Modules.AreaForecastDiscussion != nil,
|
|
spcDiscussion: ctx.Modules.SPCConvectiveDiscussion != nil,
|
|
story: ctx.Modules.WeatherStory != nil,
|
|
planning: ctx.Modules.DailyPlanning != nil,
|
|
collected: ctx.Collected.FetchedAt.Equal(collected.FetchedAt),
|
|
derived: len(ctx.Derived.DaypartSummaries) == len(derived.DaypartSummaries),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "today",
|
|
wantTitle: "Today's Weather",
|
|
wantGeneratedLabel: "Monday, June 15, 2026 at 7:14 AM",
|
|
wantDayparts: 2,
|
|
build: func(t *testing.T) result {
|
|
t.Helper()
|
|
collected := testCollected()
|
|
derived := testTodayDerived()
|
|
ctx, err := BuildTodayRenderContext(testTodayMetadata(), testTodaySnapshot(t), Today{
|
|
Summary: "Today summary.",
|
|
ForecastDiscussion: []string{"Today discussion."},
|
|
}, collected, derived)
|
|
if err != nil {
|
|
t.Fatalf("BuildTodayRenderContext() error = %v", err)
|
|
}
|
|
return result{
|
|
title: ctx.Report.Title,
|
|
forecastDateLabel: ctx.Report.ForecastDateLabel,
|
|
generatedAtLabel: ctx.Report.GeneratedAtLabel,
|
|
timezone: ctx.Report.Timezone,
|
|
validPeriod: ctx.Report.ValidPeriod,
|
|
metadata: ctx.Modules.Metadata != nil && ctx.Modules.Metadata.ReportID == report.Today,
|
|
current: ctx.Modules.CurrentConditions != nil,
|
|
hourly: ctx.Modules.HourlyForecast != nil,
|
|
dailySummary: ctx.Modules.DerivedDailySummary != nil,
|
|
daypartSummaries: ctx.Modules.DerivedDaypartSummaries != nil,
|
|
dayparts: len(ctx.Modules.Dayparts),
|
|
precip: ctx.Modules.PrecipTiming != nil,
|
|
alerts: ctx.Modules.AlertDigest != nil,
|
|
outlooks: ctx.Modules.SPCConvectiveOutlooks != nil,
|
|
discussion: ctx.Modules.AreaForecastDiscussion != nil,
|
|
spcDiscussion: ctx.Modules.SPCConvectiveDiscussion != nil,
|
|
story: ctx.Modules.WeatherStory != nil,
|
|
planning: ctx.Modules.TodayPlanning != nil,
|
|
collected: ctx.Collected.FetchedAt.Equal(collected.FetchedAt),
|
|
derived: len(ctx.Derived.DaypartSummaries) == len(derived.DaypartSummaries),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "tomorrow",
|
|
wantTitle: "Monday's Weather",
|
|
wantGeneratedLabel: "Sunday, June 14, 2026 at 9:14 AM",
|
|
wantDayparts: 2,
|
|
build: func(t *testing.T) result {
|
|
t.Helper()
|
|
collected := testCollected()
|
|
derived := testTomorrowDerived()
|
|
ctx, err := BuildTomorrowRenderContext(testTomorrowMetadata(), testTomorrowSnapshot(t), Tomorrow{
|
|
Summary: "Tomorrow summary.",
|
|
ForecastDiscussion: []string{"Tomorrow discussion."},
|
|
}, collected, derived)
|
|
if err != nil {
|
|
t.Fatalf("BuildTomorrowRenderContext() error = %v", err)
|
|
}
|
|
return result{
|
|
title: ctx.Report.Title,
|
|
forecastDateLabel: ctx.Report.ForecastDateLabel,
|
|
generatedAtLabel: ctx.Report.GeneratedAtLabel,
|
|
timezone: ctx.Report.Timezone,
|
|
validPeriod: ctx.Report.ValidPeriod,
|
|
metadata: ctx.Modules.Metadata != nil && ctx.Modules.Metadata.ReportID == report.Tomorrow,
|
|
current: ctx.Modules.CurrentConditions != nil,
|
|
hourly: ctx.Modules.HourlyForecast != nil,
|
|
dailySummary: ctx.Modules.DerivedDailySummary != nil,
|
|
daypartSummaries: ctx.Modules.DerivedDaypartSummaries != nil,
|
|
dayparts: len(ctx.Modules.Dayparts),
|
|
precip: ctx.Modules.PrecipTiming != nil,
|
|
alerts: ctx.Modules.AlertDigest != nil,
|
|
outlooks: ctx.Modules.SPCConvectiveOutlooks != nil,
|
|
discussion: ctx.Modules.AreaForecastDiscussion != nil,
|
|
spcDiscussion: ctx.Modules.SPCConvectiveDiscussion != nil,
|
|
story: ctx.Modules.WeatherStory != nil,
|
|
planning: ctx.Modules.TomorrowPlanning != nil,
|
|
collected: ctx.Collected.FetchedAt.Equal(collected.FetchedAt),
|
|
derived: len(ctx.Derived.DaypartSummaries) == len(derived.DaypartSummaries),
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result := test.build(t)
|
|
if result.title != test.wantTitle {
|
|
t.Fatalf("title = %q, want %q", result.title, test.wantTitle)
|
|
}
|
|
if result.forecastDateLabel != "Monday, June 15, 2026" {
|
|
t.Fatalf("forecastDateLabel = %q, want Monday, June 15, 2026", result.forecastDateLabel)
|
|
}
|
|
if result.generatedAtLabel != test.wantGeneratedLabel {
|
|
t.Fatalf("generatedAtLabel = %q, want %q", result.generatedAtLabel, test.wantGeneratedLabel)
|
|
}
|
|
if result.timezone != "America/Chicago" {
|
|
t.Fatalf("timezone = %q, want America/Chicago", result.timezone)
|
|
}
|
|
if !result.validPeriod.IsValid() {
|
|
t.Fatalf("validPeriod = %#v, want valid period", result.validPeriod)
|
|
}
|
|
if !result.metadata || !result.current || !result.hourly || !result.dailySummary || !result.daypartSummaries || !result.precip || !result.alerts || !result.outlooks || !result.discussion || !result.spcDiscussion || !result.story {
|
|
t.Fatalf("common modules = %#v, want all shared modules populated", result)
|
|
}
|
|
if result.dayparts != test.wantDayparts {
|
|
t.Fatalf("dayparts = %d, want %d", result.dayparts, test.wantDayparts)
|
|
}
|
|
if !result.planning {
|
|
t.Fatalf("planning = false, want %s planning module populated", test.name)
|
|
}
|
|
if !result.collected || !result.derived {
|
|
t.Fatalf("facts passthrough = collected:%t derived:%t, want both true", result.collected, result.derived)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildDailyRenderContext(t *testing.T) {
|
|
metadata := testDailyMetadata()
|
|
snapshot := testDailySnapshot(t)
|
|
generated := Daily{
|
|
Summary: "The selected day 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 := testDailyDerived()
|
|
|
|
ctx, err := BuildDailyRenderContext(metadata, snapshot, generated, collected, derived)
|
|
if err != nil {
|
|
t.Fatalf("BuildDailyRenderContext() 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 != "Saturday, June 13, 2026 at 9:14 AM" {
|
|
t.Fatalf("Report.GeneratedAtLabel = %q, want friendly generated-at label", ctx.Report.GeneratedAtLabel)
|
|
}
|
|
if !ctx.Report.ValidPeriod.Start.Equal(metadata.ValidPeriod.Start) || !ctx.Report.ValidPeriod.End.Equal(metadata.ValidPeriod.End) || ctx.Report.Timezone != "America/Chicago" {
|
|
t.Fatalf("period/timezone = %#v/%q, want metadata passthrough", ctx.Report.ValidPeriod, ctx.Report.Timezone)
|
|
}
|
|
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Daily {
|
|
t.Fatalf("Modules.Metadata = %#v, want daily 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) != 3 {
|
|
t.Fatalf("Modules.DerivedDaypartSummaries = %#v, want daypart map", ctx.Modules.DerivedDaypartSummaries)
|
|
}
|
|
if len(ctx.Modules.Dayparts) != 3 || ctx.Modules.Dayparts[0].Key != "morning" || ctx.Modules.Dayparts[1].Key != "afternoon" || ctx.Modules.Dayparts[2].Key != "evening" {
|
|
t.Fatalf("Modules.Dayparts = %#v, want derived order followed by remaining keys", ctx.Modules.Dayparts)
|
|
}
|
|
if morning := (*ctx.Modules.DerivedDaypartSummaries)["morning"]; morning.DominantConditionDisplay == "" || morning.DominantConditionLower == "" || morning.TemperatureSteadyPhraseF == "" {
|
|
t.Fatalf("daily rich morning daypart helpers = %#v, want render-context helper fields", morning)
|
|
}
|
|
if afternoon := (*ctx.Modules.DerivedDaypartSummaries)["afternoon"]; afternoon.MaxPopTimeLabel != "3:00 PM" {
|
|
t.Fatalf("daily rich afternoon daypart = %#v, want max pop time label helper", afternoon)
|
|
}
|
|
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.OutdoorWindows == nil || ctx.Modules.DailyPlanning == nil {
|
|
t.Fatalf("optional modules missing from render context: %#v", ctx.Modules)
|
|
}
|
|
if ctx.Modules.OutdoorWindows.Best == nil || ctx.Modules.OutdoorWindows.Best.Daypart != "morning" {
|
|
t.Fatalf("Modules.OutdoorWindows = %#v, want morning best window", ctx.Modules.OutdoorWindows)
|
|
}
|
|
if ctx.Modules.DailyPlanning.MorningReadiness[0] != "Take sunglasses early." {
|
|
t.Fatalf("Modules.DailyPlanning = %#v, want daily planning facts", ctx.Modules.DailyPlanning)
|
|
}
|
|
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("daily", 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",
|
|
"**Updated:** Saturday, June 13, 2026 at 9:14 AM",
|
|
"The selected day 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%.",
|
|
"- **Evening:** Mostly cloudy, with temperatures in the upper 60s.",
|
|
"## Precipitation Timing",
|
|
"- **3:00 PM** to **6:00 PM**: Expect showers. 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:**",
|
|
"- **Evening:**",
|
|
"## Precipitation Timing",
|
|
"## Forecast Discussion",
|
|
})
|
|
}
|
|
|
|
func TestBuildDailyRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|
snapshot, err := module.NewSnapshot(nil)
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
ctx, err := BuildDailyRenderContext(testDailyMetadata(), snapshot, Daily{
|
|
Summary: "Dry weather is expected for the selected day.",
|
|
ForecastDiscussion: []string{"High pressure keeps conditions quiet."},
|
|
}, testCollected(), facts.DerivedFacts{})
|
|
if err != nil {
|
|
t.Fatalf("BuildDailyRenderContext() error = %v", err)
|
|
}
|
|
if ctx.Modules.Metadata != nil || ctx.Modules.CurrentConditions != nil || ctx.Modules.PrecipTiming != nil || ctx.Modules.OutdoorWindows != nil || ctx.Modules.DailyPlanning != nil || len(ctx.Modules.Dayparts) != 0 {
|
|
t.Fatalf("Modules = %#v, want omitted optional modules", ctx.Modules)
|
|
}
|
|
rendered, err := reporttemplate.Render("daily", 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 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 morning := (*ctx.Modules.DerivedDaypartSummaries)["morning"]; morning.DominantConditionDisplay == "" || morning.DominantConditionLower == "" || morning.TemperatureSteadyPhraseF == "" {
|
|
t.Fatalf("tomorrow rich morning daypart helpers = %#v, want render-context helper fields", morning)
|
|
}
|
|
if afternoon := (*ctx.Modules.DerivedDaypartSummaries)["afternoon"]; afternoon.MaxPopTimeLabel != "3:00 PM" {
|
|
t.Fatalf("tomorrow rich afternoon daypart = %#v, want max pop time label helper", afternoon)
|
|
}
|
|
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**: Expect showers. 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.PreparedIdentity {
|
|
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
|
return briefing.PreparedIdentity{
|
|
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.PreparedIdentity {
|
|
generatedAt := time.Date(2026, 6, 14, 14, 14, 0, 0, time.UTC)
|
|
return briefing.PreparedIdentity{
|
|
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 testDailyMetadata() briefing.PreparedIdentity {
|
|
generatedAt := time.Date(2026, 6, 13, 14, 14, 0, 0, time.UTC)
|
|
return briefing.PreparedIdentity{
|
|
RunID: "run-daily",
|
|
ReportID: report.Daily,
|
|
PromptID: "weather.daily_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 testTodayMetadata() briefing.PreparedIdentity {
|
|
generatedAt := time.Date(2026, 6, 15, 12, 14, 0, 0, time.UTC)
|
|
return briefing.PreparedIdentity{
|
|
RunID: "run-today",
|
|
ReportID: report.Today,
|
|
PromptID: "weather.today_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 {
|
|
return testCivilDayDerived()
|
|
}
|
|
|
|
func testDailyDerived() facts.DerivedFacts {
|
|
return testCivilDayDerived()
|
|
}
|
|
|
|
func testTodayDerived() facts.DerivedFacts {
|
|
return testCivilDayDerived()
|
|
}
|
|
|
|
func testCivilDayDerived() 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 testTodaySnapshot(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-today",
|
|
ReportID: report.Today,
|
|
PromptID: "weather.today_generated_text",
|
|
GeneratedAt: testTodayMetadata().GeneratedAt,
|
|
Units: "imperial",
|
|
Timezone: "America/Chicago",
|
|
ValidPeriod: testTodayMetadata().ValidPeriod,
|
|
},
|
|
},
|
|
{
|
|
ID: module.CurrentConditions,
|
|
StanzaName: string(module.CurrentConditions),
|
|
Value: briefing.CurrentConditionsModule{
|
|
ConditionTextLower: "clear",
|
|
TemperatureF: intPtr(58),
|
|
ApparentTemperatureF: intPtr(57),
|
|
RelativeHumidityPercent: intPtr(61),
|
|
WindSpeedMph: intPtr(9),
|
|
WindDirectionText: "northwest",
|
|
},
|
|
},
|
|
{
|
|
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", PrecipitationType: "showers", ExpectationPhrase: "Expect showers."},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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.TodayPlanning,
|
|
StanzaName: string(module.TodayPlanning),
|
|
Value: briefing.TodayPlanningModule{
|
|
MorningReadiness: []string{"Take sunglasses early."},
|
|
OutdoorPlanning: []string{"Best outdoor window: Morning (quiet weather)."},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func testDailySnapshot(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-daily",
|
|
ReportID: report.Daily,
|
|
PromptID: "weather.daily_generated_text",
|
|
GeneratedAt: testDailyMetadata().GeneratedAt,
|
|
Units: "imperial",
|
|
Timezone: "America/Chicago",
|
|
ValidPeriod: testDailyMetadata().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,
|
|
},
|
|
"evening": {
|
|
DisplayName: "Evening",
|
|
TemperatureTrend: "steady",
|
|
TemperatureSteadyPhraseF: "upper 60s",
|
|
DominantConditionDisplay: "Mostly cloudy",
|
|
DominantConditionLower: "mostly cloudy",
|
|
},
|
|
"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", PrecipitationType: "showers", ExpectationPhrase: "Expect showers."},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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.OutdoorWindows,
|
|
StanzaName: string(module.OutdoorWindows),
|
|
Value: briefing.OutdoorWindowsModule{
|
|
Best: &briefing.OutdoorWindowModule{
|
|
Daypart: "morning",
|
|
Reasons: []string{"quiet weather"},
|
|
},
|
|
Worst: &briefing.OutdoorWindowModule{
|
|
Daypart: "afternoon",
|
|
Reasons: []string{"high precipitation chance"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: module.DailyPlanning,
|
|
StanzaName: string(module.DailyPlanning),
|
|
Value: briefing.DailyPlanningModule{
|
|
MorningReadiness: []string{"Take sunglasses early."},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSnapshot() error = %v", err)
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
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", PeriodBegins: "May 29 at 10:00 AM", PeriodEnds: "May 29 at 2:30 PM", Instruction: "Avoid low-water crossings."},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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", PrecipitationType: "showers", ExpectationPhrase: "Expect showers."},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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
|
|
}
|
|
}
|