2026-06-14 04:38:48 +00:00
package reporttemplate
import (
"encoding/json"
2026-06-15 16:23:13 +00:00
"os"
2026-06-14 04:38:48 +00:00
"strings"
"testing"
)
func TestTemplateLookup ( t * testing . T ) {
source , err := Template ( "hourly" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Updated:**" , ` {{ template "alert_digest" . }} ` , "## Current Conditions" , "## Hourly Forecast" , "## Forecast Discussion" } {
2026-06-14 04:38:48 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
}
2026-06-14 23:36:47 +00:00
func TestTomorrowTemplateLookup ( t * testing . T ) {
source , err := Template ( "tomorrow" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Forecast date:**" , ` {{ template "alert_digest" . }} ` , ` {{ template "daypart_forecast" . }} ` , ` {{ template "precipitation_timing" . }} ` , "## Forecast Discussion" } {
2026-06-14 23:36:47 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
}
2026-06-15 16:23:13 +00:00
func TestDailyTemplateLookup ( t * testing . T ) {
source , err := Template ( "daily" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Forecast date:**" , ` {{ template "alert_digest" . }} ` , ` {{ template "daypart_forecast" . }} ` , ` {{ template "precipitation_timing" . }} ` , "## Forecast Discussion" } {
2026-06-15 16:23:13 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
}
2026-06-15 14:45:49 +00:00
func TestTodayTemplateLookup ( t * testing . T ) {
source , err := Template ( "today" )
if err != nil {
t . Fatalf ( "Template() error = %v" , err )
}
2026-06-16 20:53:27 -05:00
for _ , want := range [ ] string { "# {{ .Report.Title }}" , "**Forecast date:**" , ` {{ template "alert_digest" . }} ` , "## Current Conditions" , ` {{ template "today_daypart_forecast" . }} ` , ` {{ template "precipitation_timing" . }} ` , "## Forecast Discussion" } {
2026-06-15 14:45:49 +00:00
if ! strings . Contains ( source , want ) {
t . Fatalf ( "template missing %q:\n%s" , want , source )
}
}
2026-06-15 11:01:03 -05:00
if strings . Contains ( source , "## Planning Notes" ) {
t . Fatalf ( "template includes Planning Notes section:\n%s" , source )
}
2026-06-15 14:45:49 +00:00
}
2026-06-14 04:38:48 +00:00
func TestSchemaLookup ( t * testing . T ) {
data , err := Schema ( "hourly" )
if err != nil {
t . Fatalf ( "Schema() error = %v" , err )
}
2026-06-14 23:25:55 +00:00
assertStringSchema ( t , data , "summary,forecast_discussion" , [ ] string { "summary" , "forecast_discussion" , "precipitation_timing" , "confidence" } )
}
func TestTomorrowSchemaLookup ( t * testing . T ) {
data , err := Schema ( "tomorrow" )
if err != nil {
t . Fatalf ( "Schema() error = %v" , err )
}
schema := assertSchema ( t , data , "summary,forecast_discussion" )
property , ok := schema . Properties [ "forecast_discussion" ] . ( map [ string ] any )
if ! ok {
t . Fatal ( "schema property forecast_discussion missing or invalid" )
}
if property [ "type" ] != "array" {
t . Fatalf ( "forecast_discussion type = %v, want array" , property [ "type" ] )
}
if property [ "minItems" ] != float64 ( 1 ) {
t . Fatalf ( "forecast_discussion minItems = %v, want 1" , property [ "minItems" ] )
}
items , ok := property [ "items" ] . ( map [ string ] any )
if ! ok || items [ "type" ] != "string" {
t . Fatalf ( "forecast_discussion items = %#v, want string items" , property [ "items" ] )
}
for _ , field := range [ ] string { "summary" , "precipitation_timing" , "confidence" } {
property , ok := schema . Properties [ field ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "schema property %q missing or invalid" , field )
}
if property [ "type" ] != "string" {
t . Fatalf ( "schema property %q type = %v, want string" , field , property [ "type" ] )
}
}
}
2026-06-15 16:23:13 +00:00
func TestDailySchemaLookup ( t * testing . T ) {
data , err := Schema ( "daily" )
if err != nil {
t . Fatalf ( "Schema() error = %v" , err )
}
schema := assertSchema ( t , data , "summary,forecast_discussion" )
property , ok := schema . Properties [ "forecast_discussion" ] . ( map [ string ] any )
if ! ok {
t . Fatal ( "schema property forecast_discussion missing or invalid" )
}
if property [ "type" ] != "array" {
t . Fatalf ( "forecast_discussion type = %v, want array" , property [ "type" ] )
}
if property [ "minItems" ] != float64 ( 1 ) {
t . Fatalf ( "forecast_discussion minItems = %v, want 1" , property [ "minItems" ] )
}
items , ok := property [ "items" ] . ( map [ string ] any )
if ! ok || items [ "type" ] != "string" {
t . Fatalf ( "forecast_discussion items = %#v, want string items" , property [ "items" ] )
}
for _ , field := range [ ] string { "summary" , "precipitation_timing" , "confidence" } {
property , ok := schema . Properties [ field ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "schema property %q missing or invalid" , field )
}
if property [ "type" ] != "string" {
t . Fatalf ( "schema property %q type = %v, want string" , field , property [ "type" ] )
}
}
}
2026-06-15 14:45:49 +00:00
func TestTodaySchemaLookup ( t * testing . T ) {
data , err := Schema ( "today" )
if err != nil {
t . Fatalf ( "Schema() error = %v" , err )
}
schema := assertSchema ( t , data , "summary,forecast_discussion" )
property , ok := schema . Properties [ "forecast_discussion" ] . ( map [ string ] any )
if ! ok {
t . Fatal ( "schema property forecast_discussion missing or invalid" )
}
if property [ "type" ] != "array" {
t . Fatalf ( "forecast_discussion type = %v, want array" , property [ "type" ] )
}
if property [ "minItems" ] != float64 ( 1 ) {
t . Fatalf ( "forecast_discussion minItems = %v, want 1" , property [ "minItems" ] )
}
items , ok := property [ "items" ] . ( map [ string ] any )
if ! ok || items [ "type" ] != "string" {
t . Fatalf ( "forecast_discussion items = %#v, want string items" , property [ "items" ] )
}
for _ , field := range [ ] string { "summary" , "precipitation_timing" , "confidence" } {
property , ok := schema . Properties [ field ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "schema property %q missing or invalid" , field )
}
if property [ "type" ] != "string" {
t . Fatalf ( "schema property %q type = %v, want string" , field , property [ "type" ] )
}
}
}
2026-06-14 23:25:55 +00:00
func assertStringSchema ( t * testing . T , data [ ] byte , required string , fields [ ] string ) {
t . Helper ( )
schema := assertSchema ( t , data , required )
for _ , field := range fields {
property , ok := schema . Properties [ field ] . ( map [ string ] any )
if ! ok {
t . Fatalf ( "schema property %q missing or invalid" , field )
}
if property [ "type" ] != "string" {
t . Fatalf ( "schema property %q type = %v, want string" , field , property [ "type" ] )
}
}
}
func assertSchema ( t * testing . T , data [ ] byte , required string ) struct {
Type string ` json:"type" `
AdditionalProperties bool ` json:"additionalProperties" `
Required [ ] string ` json:"required" `
Properties map [ string ] any ` json:"properties" `
} {
t . Helper ( )
2026-06-14 04:38:48 +00:00
var schema struct {
Type string ` json:"type" `
AdditionalProperties bool ` json:"additionalProperties" `
Required [ ] string ` json:"required" `
Properties map [ string ] any ` json:"properties" `
}
if err := json . Unmarshal ( data , & schema ) ; err != nil {
t . Fatalf ( "schema is invalid JSON: %v" , err )
}
if schema . Type != "object" {
t . Fatalf ( "schema type = %q, want object" , schema . Type )
}
if schema . AdditionalProperties {
t . Fatal ( "additionalProperties = true, want false" )
}
2026-06-14 23:25:55 +00:00
if strings . Join ( schema . Required , "," ) != required {
t . Fatalf ( "required = %#v, want %s" , schema . Required , required )
2026-06-14 04:38:48 +00:00
}
2026-06-14 23:25:55 +00:00
return schema
2026-06-14 04:38:48 +00:00
}
func TestRenderHourly ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
2026-06-14 08:57:53 -05:00
Report : testReportContext {
Title : "Hourly Report" ,
LocationName : "Brentwood" ,
ValidPeriodLabel : "May 29, 8:30 AM to 2:30 PM" ,
2026-06-14 12:54:32 -05:00
GeneratedAtLabel : "Saturday, June 14, 2026 at 9:14 AM" ,
2026-06-14 08:57:53 -05:00
} ,
2026-06-14 04:38:48 +00:00
GeneratedText : testGeneratedText {
2026-06-14 12:54:32 -05:00
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." ,
2026-06-14 04:38:48 +00:00
} ,
2026-06-14 08:57:53 -05:00
Modules : testModules {
CurrentConditions : & testCurrentConditions {
ConditionText : "Partly cloudy" ,
2026-06-14 12:54:32 -05:00
ConditionTextLower : "partly cloudy" ,
2026-06-14 17:40:19 -05:00
TemperatureF : intPtr ( 74 ) ,
ApparentTemperatureF : intPtr ( 76 ) ,
RelativeHumidityPercent : intPtr ( 71 ) ,
2026-06-14 08:57:53 -05:00
WindDirection : "S" ,
2026-06-14 12:54:32 -05:00
WindDirectionText : "south" ,
2026-06-14 17:40:19 -05:00
WindSpeedMph : intPtr ( 8 ) ,
2026-06-14 08:57:53 -05:00
} ,
HourlyForecast : & testHourlyForecast {
Periods : [ ] testHourlyPeriod {
2026-06-14 12:54:32 -05:00
{ HourLabel : "9:00 AM" , TextDescription : "Cloudy" , TextDescriptionLower : "cloudy" , TemperatureF : floatPtr ( 74 ) , ProbabilityOfPrecipitationPercent : floatPtr ( 19 ) , WindDirection : "S" , WindSpeedMph : floatPtr ( 8 ) } ,
{ HourLabel : "10:00 AM" , TextDescription : "Showers" , TextDescriptionLower : "showers" , TemperatureF : floatPtr ( 75 ) , ProbabilityOfPrecipitationPercent : floatPtr ( 70 ) , MentionPrecipitation : true , WindDirection : "S" , WindSpeedMph : floatPtr ( 10 ) } ,
2026-06-14 08:57:53 -05:00
} ,
} ,
PrecipTiming : & testPrecipTiming {
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTime : "10 AM" ,
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:20:26 -05:00
{ PeriodBegins : "10 AM" , PeriodBeginsHourLabel : "10:00 AM" , PeriodEnds : "12 PM" , PeriodEndsHourLabel : "12:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopTime : "10 AM" , MaxPopHourLabel : "10:00 AM" , ExpectationPhrase : "Expect showers." } ,
2026-06-14 08:57:53 -05:00
} ,
} ,
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Flood Watch" , Headline : "Flood Watch until 2:30 PM" , Severity : "Moderate" , PeriodBegins : "May 29 at 10:00 AM" , PeriodEnds : "May 29 at 2:30 PM" , Instruction : "Avoid low-water crossings." } } ,
2026-06-14 08:57:53 -05:00
} ,
SPCConvectiveOutlooks : & testSPCOutlooks {
2026-06-16 21:42:20 -05:00
Outlooks : [ ] testSPCOutlook { { LabelText : "Slight Risk" , PeriodBegins : "8 AM" , PeriodEnds : "2 PM" } } ,
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "May 29 at 8:00 AM" , PeriodEnds : "May 29 at 2:00 PM" } } ,
2026-06-14 08:57:53 -05:00
} ,
SPCConvectiveDiscussion : & testSPCDiscussion {
Discussions : [ ] testSPCDiscussionRecord { { Summary : "Strong storms may develop late morning." } } ,
} ,
AreaForecastDiscussion : & testForecastDiscussion {
KeyMessages : [ ] string { "Storms are most likely late morning." } ,
ShortTerm : "Short-term discussion favors increasing rain coverage." ,
} ,
WeatherStory : & testWeatherStory {
Available : true ,
Title : "Morning storms" ,
Description : "Morning storms remain the main story." ,
} ,
2026-06-14 04:38:48 +00:00
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"# Hourly Report" ,
2026-06-14 12:54:32 -05:00
"**Updated:** Saturday, June 14, 2026 at 9:14 AM" ,
2026-06-14 04:38:48 +00:00
"Storm chances increase through late morning." ,
2026-06-14 12:54:32 -05:00
"Currently, it is 74°F and partly cloudy. It feels like 76°F, with a relative humidity of 71% and winds from the south at 8 mph." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-16 21:09:17 -05:00
"- **Flood Watch**: Flood Watch in effect from May 29 at 10:00 AM to May 29 at 2:30 PM. Avoid low-water crossings." ,
2026-06-16 21:42:20 -05:00
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from May 29 at 8:00 AM to May 29 at 2:00 PM." ,
2026-06-14 12:54:32 -05:00
"- **9:00 AM:** 74°F and cloudy." ,
"- **10:00 AM:** 75°F and showers. Probability of precipitation is 70%." ,
2026-06-16 19:20:26 -05:00
"- **10:00 AM** to **12:00 PM**: Expect showers. The peak precipitation chance is 70% at 10:00 AM." ,
2026-06-14 12:54:32 -05:00
"A cold front is moving into the region." ,
"A front will keep the region unsettled." ,
2026-06-14 04:38:48 +00:00
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-06-14 12:54:32 -05:00
if strings . Contains ( text , "19%" ) || strings . Contains ( text , "wind S" ) || strings . Contains ( text , "## Confidence" ) {
t . Fatalf ( "rendered template included omitted details:\n%s" , text )
}
2026-06-14 05:24:25 +00:00
assertOrderedText ( t , text , [ ] string {
"# Hourly Report" ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-16 20:53:27 -05:00
"## Current Conditions" ,
2026-06-14 05:24:25 +00:00
"## Hourly Forecast" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
} )
2026-06-14 04:38:48 +00:00
}
2026-06-14 23:36:47 +00:00
func TestRenderTomorrow ( t * testing . T ) {
rendered , err := Render ( "tomorrow" , testTomorrowRenderContext {
Report : testTomorrowReportContext {
Title : "Monday's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testTomorrowGeneratedText {
Summary : "Tomorrow starts dry before showers return later in the day." ,
ForecastDiscussion : [ ] string {
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} ,
PrecipitationTiming : "A few showers may linger into early evening." ,
} ,
Modules : testTomorrowModules {
Dayparts : [ ] testTomorrowDaypart {
2026-06-14 23:26:45 -05:00
{
Key : "overnight" ,
Summary : testDaypartSummary {
DisplayName : "Overnight" ,
TemperatureTrend : "falling" ,
TemperatureStartPhraseF : "mid 60s" ,
TemperatureEndPhraseF : "upper 50s" ,
DominantConditionDisplay : "Partly cloudy" ,
DominantConditionLower : "partly cloudy" ,
} ,
} ,
2026-06-14 23:36:47 +00:00
{
Key : "morning" ,
Summary : testDaypartSummary {
2026-06-14 23:26:45 -05:00
DisplayName : "Morning" ,
TemperatureTrend : "rising" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
2026-06-14 23:36:47 +00:00
} ,
} ,
{
Key : "afternoon" ,
Summary : testDaypartSummary {
2026-06-14 23:26:45 -05:00
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 70s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
2026-06-14 23:36:47 +00:00
} ,
} ,
} ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , ExpectationPhrase : "Expect showers." } ,
2026-06-14 23:36:47 +00:00
} ,
} ,
2026-06-16 20:53:27 -05:00
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Wind Advisory" , Headline : "Wind Advisory until 8:00 PM" , Severity : "Moderate" , PeriodBegins : "June 15 at 1:00 PM" , PeriodEnds : "June 15 at 8:00 PM" , Instruction : "Secure outdoor objects." } } ,
2026-06-16 20:53:27 -05:00
} ,
2026-06-16 21:42:20 -05:00
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
2026-06-14 23:36:47 +00:00
} ,
} )
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:** Sunday, June 14, 2026 at 9:14 AM" ,
"Tomorrow starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-16 21:09:17 -05:00
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM. Secure outdoor objects." ,
2026-06-16 21:42:20 -05:00
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM." ,
2026-06-14 23:26:45 -05:00
"- **Overnight:** Partly cloudy, with temperatures falling from the mid 60s to the upper 50s." ,
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s." ,
"- **Afternoon:** Sunny, with temperatures in the upper 70s. Chance of precipitation is 70%." ,
2026-06-16 19:10:11 -05:00
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM." ,
2026-06-14 23:36:47 +00:00
"A few showers may linger into early evening." ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-06-14 23:26:45 -05:00
for _ , unwanted := range [ ] string {
"upper 50s.\n\n- **Morning:**" ,
"upper 60s.\n\n- **Afternoon:**" ,
} {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template includes blank lines between daypart bullets:\n%s" , text )
}
}
2026-06-14 23:36:47 +00:00
assertOrderedText ( t , text , [ ] string {
"# Monday's Weather" ,
2026-06-16 20:53:27 -05:00
"Tomorrow starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-14 23:36:47 +00:00
"## Daypart Forecast" ,
2026-06-14 23:26:45 -05:00
"- **Overnight:**" ,
2026-06-14 23:36:47 +00:00
"- **Morning:**" ,
"- **Afternoon:**" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} )
}
2026-06-15 16:23:13 +00:00
func TestRenderDaily ( t * testing . T ) {
rendered , err := Render ( "daily" , testDailyRenderContext {
Report : testDailyReportContext {
Title : "Monday's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testDailyGeneratedText {
Summary : "The selected day starts dry before showers return later in the day." ,
ForecastDiscussion : [ ] string {
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} ,
PrecipitationTiming : "A few showers may linger into early evening." ,
} ,
Modules : testDailyModules {
Dayparts : [ ] testDailyDaypart {
{
Key : "morning" ,
Summary : testDaypartSummary {
DisplayName : "Morning" ,
TemperatureTrend : "rising" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
} ,
} ,
{
Key : "afternoon" ,
Summary : testDaypartSummary {
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
} ,
} ,
} ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , ExpectationPhrase : "Expect showers." } ,
2026-06-15 16:23:13 +00:00
} ,
} ,
2026-06-16 20:53:27 -05:00
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Flood Watch" , Headline : "Flood Watch until 6:00 PM" , Severity : "Moderate" , PeriodBegins : "June 15 at 3:00 PM" , PeriodEnds : "June 15 at 6:00 PM" , Description : "Monitor creek levels." } } ,
2026-06-16 20:53:27 -05:00
} ,
2026-06-16 21:42:20 -05:00
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Enhanced Risk" , RiskLabel : "Enhanced risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
2026-06-15 16:23:13 +00:00
} ,
} )
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:** Sunday, June 14, 2026 at 9:14 AM" ,
"The selected day starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-16 21:09:17 -05:00
"- **Flood Watch**: Flood Watch in effect from June 15 at 3:00 PM to June 15 at 6:00 PM. Monitor creek levels." ,
2026-06-16 21:42:20 -05:00
"- **SPC Convective Outlook**: Enhanced risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM." ,
2026-06-15 16:23:13 +00:00
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s." ,
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%." ,
2026-06-16 19:10:11 -05:00
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM." ,
2026-06-15 16:23:13 +00:00
"A few showers may linger into early evening." ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
assertOrderedText ( t , text , [ ] string {
"# Monday's Weather" ,
2026-06-16 20:53:27 -05:00
"The selected day starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-15 16:23:13 +00:00
"## Daypart Forecast" ,
"- **Morning:**" ,
"- **Afternoon:**" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
} )
}
2026-06-15 14:45:49 +00:00
func TestRenderToday ( t * testing . T ) {
rendered , err := Render ( "today" , testTodayRenderContext {
Report : testTodayReportContext {
Title : "Today's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Monday, June 15, 2026 at 7:14 AM" ,
} ,
GeneratedText : testTomorrowGeneratedText {
Summary : "Today starts dry before showers return later in the day." ,
ForecastDiscussion : [ ] string {
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} ,
PrecipitationTiming : "A few showers may linger into early evening." ,
} ,
Modules : testTodayModules {
CurrentConditions : & testCurrentConditions {
2026-06-15 11:01:03 -05:00
ConditionTextLower : "clear" ,
TemperatureF : intPtr ( 58 ) ,
ApparentTemperatureF : intPtr ( 57 ) ,
RelativeHumidityPercent : intPtr ( 61 ) ,
WindDirectionText : "northwest" ,
WindSpeedMph : intPtr ( 9 ) ,
2026-06-15 14:45:49 +00:00
} ,
Dayparts : [ ] testTomorrowDaypart {
{
Key : "morning" ,
Summary : testDaypartSummary {
DisplayName : "Morning" ,
TemperatureTrend : "rising" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
DominantConditionDisplay : "Sunny" ,
DominantConditionLower : "sunny" ,
} ,
} ,
{
Key : "afternoon" ,
Summary : testDaypartSummary {
DisplayName : "Afternoon" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "upper 70s" ,
DominantConditionDisplay : "Showers" ,
DominantConditionLower : "showers" ,
MaxPopPercent : intPtr ( 70 ) ,
MaxPopTimeLabel : "3:00 PM" ,
MentionPrecipitation : true ,
} ,
} ,
2026-06-15 11:01:03 -05:00
{
Key : "evening" ,
Summary : testDaypartSummary {
DisplayName : "Evening" ,
} ,
} ,
2026-06-15 14:45:49 +00:00
} ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
2026-06-16 19:10:11 -05:00
{ PeriodBeginsHourLabel : "3:00 PM" , PeriodEndsHourLabel : "6:00 PM" , MaxPopPercent : intPtr ( 70 ) , MaxPopHourLabel : "3:00 PM" , ExpectationPhrase : "Expect showers." } ,
2026-06-15 14:45:49 +00:00
} ,
} ,
2026-06-16 20:53:27 -05:00
AlertDigest : & testAlertDigest {
2026-06-16 21:09:17 -05:00
Relevant : [ ] testAlert { { Event : "Wind Advisory" , Headline : "Wind Advisory until 8:00 PM" , Severity : "Moderate" , PeriodBegins : "June 15 at 1:00 PM" , PeriodEnds : "June 15 at 8:00 PM" , Instruction : "Secure outdoor objects." } } ,
2026-06-16 20:53:27 -05:00
} ,
2026-06-16 21:42:20 -05:00
SPCConvectiveOutlooks : & testSPCOutlooks {
RiskDigest : [ ] testSPCRiskDigest { { LabelText : "Slight Risk" , RiskLabel : "Slight risk" , PeriodBegins : "June 15 at 7:00 AM" , PeriodEnds : "June 16 at 7:00 AM" } } ,
} ,
2026-06-15 14:45:49 +00:00
TodayPlanning : & testTodayPlanning {
MorningReadiness : [ ] string { "Morning weather looks routine." } ,
LateDayChangeWatch : [ ] string { "Watch late-day shower timing." } ,
} ,
} ,
} )
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" ,
"**Updated:** Monday, June 15, 2026 at 7:14 AM" ,
"Today starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-16 21:09:17 -05:00
"- **Wind Advisory**: Wind Advisory in effect from June 15 at 1:00 PM to June 15 at 8:00 PM. Secure outdoor objects." ,
2026-06-16 21:42:20 -05:00
"- **SPC Convective Outlook**: Slight risk for severe thunderstorms in effect from June 15 at 7:00 AM to June 16 at 7:00 AM." ,
2026-06-15 11:01:03 -05:00
"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." ,
2026-06-15 14:45:49 +00:00
"- **Morning:** Sunny, with temperatures rising from the upper 50s to the upper 60s." ,
"- **Afternoon:** Showers, with temperatures in the upper 70s. Chance of precipitation is 70%." ,
2026-06-16 19:10:11 -05:00
"- **3:00 PM** to **6:00 PM**: Expect showers. The peak precipitation chance is 70% at 3:00 PM." ,
2026-06-15 14:45:49 +00:00
"A few showers may linger into early evening." ,
"Clouds increase after sunrise." ,
"Rain chances peak during the afternoon." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
assertOrderedText ( t , text , [ ] string {
"# Today's Weather" ,
2026-06-16 20:53:27 -05:00
"Today starts dry before showers return later in the day." ,
2026-06-16 21:42:20 -05:00
"## Alert Digest" ,
2026-06-15 14:45:49 +00:00
"## Current Conditions" ,
"## Daypart Forecast" ,
"- **Morning:**" ,
"- **Afternoon:**" ,
"## Precipitation Timing" ,
"## Forecast Discussion" ,
} )
2026-06-15 11:01:03 -05:00
for _ , unwanted := range [ ] string { "## Planning Notes" , "Morning weather looks routine." , "Watch late-day shower timing." , "- **Evening:**" , "Forecast details are limited" } {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "rendered template included %q:\n%s" , unwanted , text )
}
}
2026-06-15 14:45:49 +00:00
}
2026-06-15 20:38:56 +00:00
func TestRenderDaypartTemplatesUseRichHelperFields ( t * testing . T ) {
tests := [ ] struct {
name string
template string
context any
want [ ] string
} {
{
name : "daily" ,
template : "daily" ,
context : testDailyRenderContext {
Report : testDailyReportContext { Title : "Daily" , ForecastDateLabel : "Monday, June 15, 2026" } ,
GeneratedText : testDailyGeneratedText { Summary : "Daily summary." } ,
Modules : testDailyModules { Dayparts : [ ] testDailyDaypart {
{ Key : "morning" , Summary : testDaypartSummary {
DisplayName : "Morning" ,
DominantConditionDisplay : "Showers" ,
TemperaturePhraseF : "upper 60s" ,
} } ,
} } ,
} ,
want : [ ] string { "- **Morning:** Showers, with temperatures in the upper 60s." } ,
} ,
{
name : "today" ,
template : "today" ,
context : testTodayRenderContext {
Report : testTodayReportContext { Title : "Today" , ForecastDateLabel : "Monday, June 15, 2026" } ,
GeneratedText : testTomorrowGeneratedText { Summary : "Today summary." } ,
Modules : testTodayModules { Dayparts : [ ] testTomorrowDaypart {
{ Key : "afternoon" , Summary : testDaypartSummary {
DisplayName : "Afternoon" ,
DominantConditionDisplay : "Storms" ,
TemperaturePeakPhraseF : "low 80s" ,
TemperatureTrend : "peaking" ,
MaxPopPercent : intPtr ( 70 ) ,
MentionPrecipitation : true ,
} } ,
} } ,
} ,
want : [ ] string { "- **Afternoon:** Storms, with temperatures peaking in the low 80s. Chance of precipitation is 70%." } ,
} ,
{
name : "tomorrow" ,
template : "tomorrow" ,
context : testTomorrowRenderContext {
Report : testTomorrowReportContext { Title : "Tomorrow" , ForecastDateLabel : "Tuesday, June 16, 2026" } ,
GeneratedText : testTomorrowGeneratedText { Summary : "Tomorrow summary." } ,
Modules : testTomorrowModules { Dayparts : [ ] testTomorrowDaypart {
{ Key : "morning" , Summary : testDaypartSummary {
DisplayName : "Morning" ,
DominantConditionDisplay : "Clear" ,
TemperatureStartPhraseF : "upper 50s" ,
TemperatureEndPhraseF : "upper 60s" ,
TemperatureTrend : "rising" ,
} } ,
} } ,
} ,
want : [ ] string { "- **Morning:** Clear, with temperatures rising from the upper 50s to the upper 60s." } ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
rendered , err := Render ( tt . template , tt . context )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range tt . want {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
2026-06-16 21:42:20 -05:00
if strings . Contains ( text , "## Alert Digest" ) {
t . Fatalf ( "rendered template includes empty Alerts and Risk Products section:\n%s" , text )
2026-06-16 20:53:27 -05:00
}
if strings . Contains ( text , "\n\n\n" ) {
t . Fatalf ( "rendered template includes excess blank lines:\n%s" , text )
}
2026-06-15 20:38:56 +00:00
} )
}
}
2026-06-14 23:36:47 +00:00
func TestRenderTomorrowOmitsPrecipitationTimingWithoutWindows ( t * testing . T ) {
rendered , err := Render ( "tomorrow" , testTomorrowRenderContext {
Report : testTomorrowReportContext {
Title : "Monday's Weather" ,
ForecastDateLabel : "Monday, June 15, 2026" ,
GeneratedAtLabel : "Sunday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testTomorrowGeneratedText {
Summary : "Dry weather is expected tomorrow." ,
ForecastDiscussion : [ ] string { "High pressure keeps rain chances low." } ,
PrecipitationTiming : "Any stray shower chance is too low to highlight." ,
} ,
Modules : testTomorrowModules {
Dayparts : [ ] testTomorrowDaypart {
{
Key : "morning" ,
Summary : testDaypartSummary {
2026-06-14 23:26:45 -05:00
DisplayName : "Morning" ,
TemperatureTrend : "steady" ,
TemperatureSteadyPhraseF : "low 60s" ,
DominantConditionDisplay : "Clear" ,
DominantConditionLower : "clear" ,
2026-06-14 23:36:47 +00:00
} ,
} ,
} ,
PrecipTiming : & testPrecipTiming { } ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , unwanted := range [ ] string { "## Precipitation Timing" , "Any stray shower chance is too low to highlight." } {
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "dry render includes %q:\n%s" , unwanted , text )
}
}
}
2026-06-14 12:54:32 -05:00
func TestRenderHourlyOmitsConditionalSectionsForClearWeather ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
Report : testReportContext {
Title : "Hourly Report" ,
GeneratedAtLabel : "Saturday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testGeneratedText {
Summary : "Dry weather is expected through the next several hours." ,
ForecastDiscussion : "Quiet conditions should persist through midday." ,
} ,
Modules : testModules {
CurrentConditions : & testCurrentConditions {
ConditionTextLower : "cloudy" ,
2026-06-14 17:40:19 -05:00
TemperatureF : intPtr ( 72 ) ,
2026-06-14 12:54:32 -05:00
} ,
HourlyForecast : & testHourlyForecast {
Periods : [ ] testHourlyPeriod {
{ HourLabel : "9:00 AM" , TextDescriptionLower : "mostly cloudy" , TemperatureF : floatPtr ( 71 ) , ProbabilityOfPrecipitationPercent : floatPtr ( 10 ) } ,
} ,
} ,
AlertDigest : & testAlertDigest { } ,
PrecipTiming : & testPrecipTiming {
MaxPopPercent : intPtr ( 10 ) ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
2026-06-16 21:42:20 -05:00
for _ , unwanted := range [ ] string { "## Alert Digest" , "## Precipitation Timing" , "Probability of precipitation is 10%" , "wind" } {
2026-06-14 12:54:32 -05:00
if strings . Contains ( text , unwanted ) {
t . Fatalf ( "clear render includes %q:\n%s" , unwanted , text )
}
}
2026-06-16 20:44:28 -05:00
if strings . Contains ( text , "\n\n\n" ) {
t . Fatalf ( "clear render includes excess blank lines:\n%s" , text )
}
for _ , want := range [ ] string {
"Currently, it is 72°F and cloudy.\n\n## Hourly Forecast" ,
"- **9:00 AM:** 71°F and mostly cloudy.\n\n## Forecast Discussion" ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "clear render missing spacing %q:\n%s" , want , text )
}
}
2026-06-14 12:54:32 -05:00
}
2026-06-16 19:20:26 -05:00
func TestRenderHourlyUsesSharedOpenEndedPrecipitationTiming ( t * testing . T ) {
rendered , err := Render ( "hourly" , testRenderContext {
Report : testReportContext {
Title : "Hourly Report" ,
GeneratedAtLabel : "Saturday, June 14, 2026 at 9:14 AM" ,
} ,
GeneratedText : testGeneratedText {
Summary : "Rain chances increase through midday." ,
ForecastDiscussion : "Showers may continue beyond the report period." ,
PrecipitationTiming : "Plan for wet roads through the end of the period." ,
} ,
Modules : testModules {
CurrentConditions : & testCurrentConditions { } ,
HourlyForecast : & testHourlyForecast { } ,
AlertDigest : & testAlertDigest { } ,
PrecipTiming : & testPrecipTiming {
PrecipitationWindows : [ ] testPrecipWindow {
{
PeriodBeginsHourLabel : "8:00 AM" ,
MaxPopPercent : intPtr ( 60 ) ,
MaxPopHourLabel : "10:00 AM" ,
ExpectationPhrase : "Showers likely." ,
} ,
} ,
} ,
} ,
} )
if err != nil {
t . Fatalf ( "Render() error = %v" , err )
}
text := string ( rendered )
for _ , want := range [ ] string {
"## Precipitation Timing" ,
"- **Starting at 8:00 AM**: Showers likely. The peak precipitation chance is 60% at 10:00 AM." ,
"Plan for wet roads through the end of the period." ,
} {
if ! strings . Contains ( text , want ) {
t . Fatalf ( "rendered template missing %q:\n%s" , want , text )
}
}
}
2026-06-14 04:38:48 +00:00
func TestUnknownAssetsReturnActionableErrors ( t * testing . T ) {
2026-06-15 16:23:13 +00:00
if _ , err := Template ( "missing" ) ; err == nil || ! strings . Contains ( err . Error ( ) , ` unknown report template "missing" ` ) {
2026-06-14 04:38:48 +00:00
t . Fatalf ( "Template() error = %v, want unknown template" , err )
}
2026-06-15 16:23:13 +00:00
if _ , err := Schema ( "missing" ) ; err == nil || ! strings . Contains ( err . Error ( ) , ` unknown generated text schema "missing" ` ) {
2026-06-14 04:38:48 +00:00
t . Fatalf ( "Schema() error = %v, want unknown schema" , err )
}
2026-06-15 16:23:13 +00:00
if _ , err := Render ( "missing" , testRenderContext { } ) ; err == nil || ! strings . Contains ( err . Error ( ) , ` unknown report template "missing" ` ) {
2026-06-14 04:38:48 +00:00
t . Fatalf ( "Render() error = %v, want unknown template" , err )
}
}
2026-06-15 16:23:13 +00:00
func TestDailyPromptAssetExists ( t * testing . T ) {
data , err := os . ReadFile ( "prompts/daily.generated_text.md" )
if err != nil {
t . Fatalf ( "read Daily prompt asset: %v" , err )
}
for _ , want := range [ ] string { "TASK:" , "`summary`" , "`forecast_discussion`" , "`daily_planning`" } {
if ! strings . Contains ( string ( data ) , want ) {
t . Fatalf ( "Daily prompt asset missing %q:\n%s" , want , string ( data ) )
}
}
}
2026-06-14 04:38:48 +00:00
func TestRenderFailsForMissingContextFields ( t * testing . T ) {
2026-06-14 08:57:53 -05:00
_ , err := Render ( "hourly" , map [ string ] any { "Report" : map [ string ] any { "Title" : "Hourly Report" } } )
2026-06-14 04:38:48 +00:00
if err == nil {
t . Fatal ( "Render() error = nil, want missing field error" )
}
if ! strings . Contains ( err . Error ( ) , ` render report template "hourly" ` ) {
t . Fatalf ( "Render() error = %v, want render context" , err )
}
}
type testRenderContext struct {
2026-06-14 08:57:53 -05:00
Report testReportContext
GeneratedText testGeneratedText
Modules testModules
}
2026-06-14 23:36:47 +00:00
type testTomorrowRenderContext struct {
Report testTomorrowReportContext
GeneratedText testTomorrowGeneratedText
Modules testTomorrowModules
}
2026-06-15 14:45:49 +00:00
type testTodayRenderContext struct {
Report testTodayReportContext
GeneratedText testTomorrowGeneratedText
Modules testTodayModules
}
2026-06-15 16:23:13 +00:00
type testDailyRenderContext struct {
Report testDailyReportContext
GeneratedText testDailyGeneratedText
Modules testDailyModules
}
2026-06-14 08:57:53 -05:00
type testReportContext struct {
Title string
LocationName string
ValidPeriodLabel string
GeneratedAtLabel string
2026-06-14 04:38:48 +00:00
}
type testGeneratedText struct {
2026-06-14 12:54:32 -05:00
Summary string
ForecastDiscussion string
PrecipitationTiming string
Confidence string
2026-06-14 04:38:48 +00:00
}
2026-06-14 23:36:47 +00:00
type testTomorrowReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
2026-06-15 14:45:49 +00:00
type testTodayReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
2026-06-15 16:23:13 +00:00
type testDailyReportContext struct {
Title string
ForecastDateLabel string
GeneratedAtLabel string
}
2026-06-14 23:36:47 +00:00
type testTomorrowGeneratedText struct {
Summary string
ForecastDiscussion [ ] string
PrecipitationTiming string
Confidence string
}
2026-06-15 16:23:13 +00:00
type testDailyGeneratedText struct {
Summary string
ForecastDiscussion [ ] string
PrecipitationTiming string
Confidence string
}
2026-06-14 08:57:53 -05:00
type testModules struct {
CurrentConditions * testCurrentConditions
HourlyForecast * testHourlyForecast
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
AreaForecastDiscussion * testForecastDiscussion
SPCConvectiveDiscussion * testSPCDiscussion
WeatherStory * testWeatherStory
}
2026-06-14 23:36:47 +00:00
type testTomorrowModules struct {
2026-06-16 21:42:20 -05:00
Dayparts [ ] testTomorrowDaypart
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
2026-06-14 23:36:47 +00:00
}
2026-06-15 14:45:49 +00:00
type testTodayModules struct {
2026-06-16 21:42:20 -05:00
CurrentConditions * testCurrentConditions
Dayparts [ ] testTomorrowDaypart
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
TodayPlanning * testTodayPlanning
2026-06-15 14:45:49 +00:00
}
2026-06-15 16:23:13 +00:00
type testDailyModules struct {
2026-06-16 21:42:20 -05:00
Dayparts [ ] testDailyDaypart
PrecipTiming * testPrecipTiming
AlertDigest * testAlertDigest
SPCConvectiveOutlooks * testSPCOutlooks
2026-06-15 16:23:13 +00:00
}
2026-06-15 14:45:49 +00:00
type testTodayPlanning struct {
MorningReadiness [ ] string
CommuteSchoolWorkdayConcerns [ ] string
OutdoorPlanning [ ] string
LateDayChangeWatch [ ] string
}
2026-06-14 23:36:47 +00:00
type testTomorrowDaypart struct {
Key string
Summary testDaypartSummary
}
2026-06-15 16:23:13 +00:00
type testDailyDaypart struct {
Key string
Summary testDaypartSummary
}
2026-06-14 23:36:47 +00:00
type testDaypartSummary struct {
2026-06-14 23:26:45 -05:00
DisplayName string
TempRangeF string
TemperaturePhraseF string
TemperatureTrend string
TemperatureStartPhraseF string
TemperatureEndPhraseF string
TemperaturePeakPhraseF string
TemperatureSteadyPhraseF string
MaxPopPercent * int
MaxPopTime string
MaxPopTimeLabel string
MentionPrecipitation bool
DominantCondition string
DominantConditionLower string
DominantConditionDisplay string
2026-06-14 23:36:47 +00:00
}
2026-06-14 08:57:53 -05:00
type testCurrentConditions struct {
ConditionText string
2026-06-14 12:54:32 -05:00
ConditionTextLower string
2026-06-14 17:40:19 -05:00
TemperatureF * int
ApparentTemperatureF * int
RelativeHumidityPercent * int
WindSpeedMph * int
2026-06-14 08:57:53 -05:00
WindDirection string
2026-06-14 12:54:32 -05:00
WindDirectionText string
2026-06-14 08:57:53 -05:00
}
type testHourlyForecast struct {
Periods [ ] testHourlyPeriod
}
type testHourlyPeriod struct {
2026-06-14 12:54:32 -05:00
HourLabel string
2026-06-14 08:57:53 -05:00
PeriodBegins string
Name string
TextDescription string
2026-06-14 12:54:32 -05:00
TextDescriptionLower string
2026-06-14 08:57:53 -05:00
TemperatureF * float64
WindSpeedMph * float64
WindGustMph * float64
WindDirection string
ProbabilityOfPrecipitationPercent * float64
2026-06-14 12:54:32 -05:00
MentionPrecipitation bool
2026-06-14 08:57:53 -05:00
}
type testPrecipTiming struct {
MaxPopPercent * int
MaxPopTime string
PrecipitationWindows [ ] testPrecipWindow
ThunderMentioned bool
}
type testPrecipWindow struct {
2026-06-14 12:54:32 -05:00
PeriodBegins string
PeriodBeginsHourLabel string
PeriodEnds string
PeriodEndsHourLabel string
MaxPopPercent * int
MaxPopTime string
MaxPopHourLabel string
2026-06-16 19:10:11 -05:00
PrecipitationType string
ExpectationPhrase string
2026-06-14 08:57:53 -05:00
}
type testAlertDigest struct {
Missing bool
Relevant [ ] testAlert
}
type testAlert struct {
2026-06-16 21:09:17 -05:00
Event string
Headline string
Severity string
PeriodBegins string
PeriodEnds string
Instruction string
Description string
2026-06-14 08:57:53 -05:00
}
type testSPCOutlooks struct {
2026-06-16 21:42:20 -05:00
Outlooks [ ] testSPCOutlook
RiskDigest [ ] testSPCRiskDigest
2026-06-14 08:57:53 -05:00
}
type testSPCOutlook struct {
Label string
LabelText string
OutlookType string
PeriodBegins string
PeriodEnds string
2026-06-14 04:38:48 +00:00
}
2026-06-16 21:42:20 -05:00
type testSPCRiskDigest struct {
LabelText string
RiskLabel string
PeriodBegins string
PeriodEnds string
}
2026-06-14 04:38:48 +00:00
type testForecastDiscussion struct {
KeyMessages [ ] string
ShortTerm string
}
2026-06-14 05:24:25 +00:00
2026-06-14 08:57:53 -05:00
type testSPCDiscussion struct {
Discussions [ ] testSPCDiscussionRecord
}
type testSPCDiscussionRecord struct {
Headline string
Summary string
Discussion string
}
type testWeatherStory struct {
Available bool
Title string
Description string
}
func floatPtr ( value float64 ) * float64 {
return & value
}
func intPtr ( value int ) * int {
return & value
}
2026-06-14 05:24:25 +00:00
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
}
}