Add SPC convective workflow coverage
This commit is contained in:
@@ -169,9 +169,13 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
!strings.Contains(string(data), "current_conditions:") ||
|
||||
!strings.Contains(string(data), "narrative_forecast:") ||
|
||||
!strings.Contains(string(data), "hourly_forecast:") ||
|
||||
!strings.Contains(string(data), "area_forecast_discussion:") {
|
||||
!strings.Contains(string(data), "area_forecast_discussion:") ||
|
||||
!strings.Contains(string(data), "spc_convective_outlooks:") {
|
||||
t.Fatalf("data package missing expected content:\n%s", string(data))
|
||||
}
|
||||
if strings.Contains(string(data), "spc_convective_discussion:") {
|
||||
t.Fatalf("data package has SPC convective discussion, want omitted for empty checked source:\n%s", string(data))
|
||||
}
|
||||
if strings.Contains(string(data), "source_warnings:") {
|
||||
t.Fatalf("data package has source warnings, want none for complete fetched sources:\n%s", string(data))
|
||||
}
|
||||
@@ -184,9 +188,10 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
storyIndex := strings.Index(string(data), " weather_story:")
|
||||
currentIndex := strings.Index(string(data), " current_conditions:")
|
||||
hourlyIndex := strings.Index(string(data), " hourly_forecast:")
|
||||
if riskIndex < 0 || derivedIndex < 0 || narrativeIndex < 0 || rawIndex < 0 || alertIndex < 0 || summaryIndex < 0 || storyIndex < 0 || currentIndex < 0 || hourlyIndex < 0 ||
|
||||
outlookIndex := strings.Index(string(data), " spc_convective_outlooks:")
|
||||
if riskIndex < 0 || derivedIndex < 0 || narrativeIndex < 0 || rawIndex < 0 || alertIndex < 0 || outlookIndex < 0 || summaryIndex < 0 || storyIndex < 0 || currentIndex < 0 || hourlyIndex < 0 ||
|
||||
!(riskIndex < derivedIndex && derivedIndex < narrativeIndex && narrativeIndex < rawIndex) ||
|
||||
!(riskIndex < alertIndex && derivedIndex < summaryIndex && narrativeIndex < storyIndex && rawIndex < currentIndex && currentIndex < hourlyIndex) {
|
||||
!(riskIndex < alertIndex && alertIndex < outlookIndex && outlookIndex < derivedIndex && derivedIndex < summaryIndex && narrativeIndex < storyIndex && rawIndex < currentIndex && currentIndex < hourlyIndex) {
|
||||
t.Fatalf("data package grouping is wrong, want categorized prompt stanzas:\n%s", string(data))
|
||||
}
|
||||
savedDataPackage, err := promptinput.LoadYAML(data)
|
||||
@@ -199,6 +204,10 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
if _, ok := savedDataPackage.Briefing.Values["metadata"]; !ok {
|
||||
t.Fatal("data package metadata stanza missing")
|
||||
}
|
||||
spcOutlooks, ok := savedDataPackage.Briefing.Values["spc_convective_outlooks"].(map[string]any)
|
||||
if !ok || spcOutlooks["checked"] != true || spcOutlooks["outlook_count"] != 0 {
|
||||
t.Fatalf("data package SPC convective outlooks = %#v, want checked empty source", savedDataPackage.Briefing.Values["spc_convective_outlooks"])
|
||||
}
|
||||
current, ok := savedDataPackage.Briefing.Values["current_conditions"].(map[string]any)
|
||||
if !ok || current["condition_text"] != "Clear" {
|
||||
t.Fatalf("data package current conditions = %#v, want current conditions", savedDataPackage.Briefing.Values["current_conditions"])
|
||||
@@ -246,6 +255,86 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportIncludesSPCConvectivePromptStanzas(t *testing.T) {
|
||||
server := dailyBundleServerWithConvectiveResponse(t, qualifyingConvectiveOutlooksResponse)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
|
||||
result := generateDailyReportForTest(t, cfg)
|
||||
if _, ok := result.ModuleSnapshot.LookupStanza("spc_convective_outlooks"); !ok {
|
||||
t.Fatal("module snapshot missing spc_convective_outlooks stanza")
|
||||
}
|
||||
if _, ok := result.ModuleSnapshot.LookupStanza("spc_convective_discussion"); !ok {
|
||||
t.Fatal("module snapshot missing spc_convective_discussion stanza")
|
||||
}
|
||||
data := readDataPackageForTest(t, result)
|
||||
text := string(data)
|
||||
if strings.Contains(text, "geometry:") || strings.Contains(text, "coordinates:") || strings.Contains(text, "Polygon") {
|
||||
t.Fatalf("data package contains geometry, want prompt-facing fields only:\n%s", text)
|
||||
}
|
||||
for _, want := range []string{
|
||||
" spc_convective_outlooks:",
|
||||
" spc_convective_discussion:",
|
||||
" included_because: severity_rank >= 3",
|
||||
" label_text: Slight Risk",
|
||||
" severity_rank: 3",
|
||||
" discussion: Severe thunderstorms may produce damaging winds during the afternoon.",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("data package missing %q:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
|
||||
riskIndex := strings.Index(text, " applicable_risk_products:")
|
||||
alertIndex := strings.Index(text, " alert_digest:")
|
||||
outlookIndex := strings.Index(text, " spc_convective_outlooks:")
|
||||
derivedIndex := strings.Index(text, " derived_summaries:")
|
||||
narrativeIndex := strings.Index(text, " narrative_products:")
|
||||
forecastIndex := strings.Index(text, " narrative_forecast:")
|
||||
afdIndex := strings.Index(text, " area_forecast_discussion:")
|
||||
discussionIndex := strings.Index(text, " spc_convective_discussion:")
|
||||
storyIndex := strings.Index(text, " weather_story:")
|
||||
rawIndex := strings.Index(text, " raw_data:")
|
||||
if riskIndex < 0 || alertIndex < 0 || outlookIndex < 0 || derivedIndex < 0 || narrativeIndex < 0 || forecastIndex < 0 || afdIndex < 0 || discussionIndex < 0 || storyIndex < 0 || rawIndex < 0 ||
|
||||
!(riskIndex < alertIndex && alertIndex < outlookIndex && outlookIndex < derivedIndex) ||
|
||||
!(narrativeIndex < forecastIndex && forecastIndex < afdIndex && afdIndex < discussionIndex && discussionIndex < storyIndex && storyIndex < rawIndex) {
|
||||
t.Fatalf("data package category order is wrong:\n%s", text)
|
||||
}
|
||||
|
||||
loaded, err := promptinput.LoadYAML(data)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadYAML() error = %v", err)
|
||||
}
|
||||
if _, ok := loaded.Briefing.Values["spc_convective_outlooks"]; !ok {
|
||||
t.Fatal("loaded package missing spc_convective_outlooks stanza")
|
||||
}
|
||||
if _, ok := loaded.Briefing.Values["spc_convective_discussion"]; !ok {
|
||||
t.Fatal("loaded package missing spc_convective_discussion stanza")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportOmitsSPCConvectiveDiscussionBelowThreshold(t *testing.T) {
|
||||
server := dailyBundleServerWithConvectiveResponse(t, lowerRiskConvectiveOutlooksResponse)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
|
||||
result := generateDailyReportForTest(t, cfg)
|
||||
if _, ok := result.ModuleSnapshot.LookupStanza("spc_convective_outlooks"); !ok {
|
||||
t.Fatal("module snapshot missing spc_convective_outlooks stanza")
|
||||
}
|
||||
if _, ok := result.ModuleSnapshot.LookupStanza("spc_convective_discussion"); ok {
|
||||
t.Fatal("module snapshot has spc_convective_discussion stanza, want omitted below threshold")
|
||||
}
|
||||
text := string(readDataPackageForTest(t, result))
|
||||
if !strings.Contains(text, " spc_convective_outlooks:") || !strings.Contains(text, " label_text: Marginal Risk") || !strings.Contains(text, " severity_rank: 2") {
|
||||
t.Fatalf("data package missing lower-risk SPC outlook:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "spc_convective_discussion:") || strings.Contains(text, "Low-end severe threat discussion.") {
|
||||
t.Fatalf("data package has SPC convective discussion, want omitted below threshold:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "geometry:") || strings.Contains(text, "coordinates:") || strings.Contains(text, "Polygon") {
|
||||
t.Fatalf("data package contains geometry, want prompt-facing fields only:\n%s", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
@@ -1119,6 +1208,11 @@ func moduleIDsForTest(ids []module.ID) []string {
|
||||
}
|
||||
|
||||
func dailyBundleServer(t *testing.T) *httptest.Server {
|
||||
t.Helper()
|
||||
return dailyBundleServerWithConvectiveResponse(t, emptyConvectiveOutlooksResponse)
|
||||
}
|
||||
|
||||
func dailyBundleServerWithConvectiveResponse(t *testing.T, convectiveResponse string) *httptest.Server {
|
||||
t.Helper()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -1137,7 +1231,7 @@ func dailyBundleServer(t *testing.T) *httptest.Server {
|
||||
case "/weatherstories/latest":
|
||||
_, _ = w.Write([]byte(`{"data":{"officeId":"LSX","startTime":"2026-05-30T08:46:00Z","endTime":"2026-05-31T11:00:00Z","updatedAt":"2026-05-30T09:00:34Z","title":"Several Chances for Rain Through Monday","description":"Scattered showers and thunderstorms remain possible.","altText":"Forecast weather story graphic.","priority":false,"order":1,"downloadUrl":"https://api.weather.gov/offices/LSX/weatherstories/download/test"}}`))
|
||||
case "/outlooks/convective":
|
||||
_, _ = w.Write([]byte(`{"data":{"asOf":"2026-05-29T16:00:00Z","outlooks":[],"discussions":[]}}`))
|
||||
_, _ = w.Write([]byte(convectiveResponse))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
@@ -1146,6 +1240,12 @@ func dailyBundleServer(t *testing.T) *httptest.Server {
|
||||
return server
|
||||
}
|
||||
|
||||
const emptyConvectiveOutlooksResponse = `{"data":{"asOf":"2026-05-29T16:00:00Z","outlooks":[],"discussions":[]}}`
|
||||
|
||||
const qualifyingConvectiveOutlooksResponse = `{"data":{"locationId":"home","locationName":"Brentwood","asOf":"2026-05-29T16:00:00Z","issuedAt":"2026-05-29T15:45:00Z","updatedAt":"2026-05-29T16:05:00Z","outlooks":[{"id":"day1-categorical","day":1,"outlookType":"categorical","label":"SLGT","labelText":"Slight Risk","severityRank":3,"validFrom":"2026-05-29T11:00:00-05:00","validTo":"2026-05-30T07:00:00-05:00","issuedAt":"2026-05-29T10:45:00-05:00","expiresAt":"2026-05-30T07:00:00-05:00","containsLocation":true,"sourceUrl":"https://www.spc.noaa.gov/products/outlook/day1otlk.html","imageUrl":"https://www.spc.noaa.gov/products/outlook/day1probotlk.gif","geometry":{"type":"Polygon","coordinates":[[[-91.0,38.0],[-90.0,38.0],[-90.0,39.0],[-91.0,39.0],[-91.0,38.0]]]}}],"discussions":[{"day":1,"headline":"Severe storms possible","summary":"Scattered severe storms are possible.","discussion":"Severe thunderstorms may produce damaging winds during the afternoon.","updatedAt":"2026-05-29T11:15:00-05:00"}]}}`
|
||||
|
||||
const lowerRiskConvectiveOutlooksResponse = `{"data":{"locationId":"home","locationName":"Brentwood","asOf":"2026-05-29T16:00:00Z","issuedAt":"2026-05-29T15:45:00Z","outlooks":[{"id":"day1-categorical","day":1,"outlookType":"categorical","label":"MRGL","labelText":"Marginal Risk","severityRank":2,"validFrom":"2026-05-29T11:00:00-05:00","validTo":"2026-05-30T07:00:00-05:00","containsLocation":true,"geometry":{"type":"Polygon","coordinates":[[[-91.0,38.0],[-90.0,38.0],[-90.0,39.0],[-91.0,39.0],[-91.0,38.0]]]}}],"discussions":[{"day":1,"headline":"Low-end severe threat","summary":"An isolated severe storm cannot be ruled out.","discussion":"Low-end severe threat discussion.","updatedAt":"2026-05-29T11:15:00-05:00"}]}}`
|
||||
|
||||
func TestResolveGenerateStorm(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
@@ -1339,6 +1439,37 @@ func dailyTestConfig(t *testing.T, server *httptest.Server) config.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult {
|
||||
t.Helper()
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
Renderer: successfulRenderer("# Daily Report\n"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func readDataPackageForTest(t *testing.T, result *ReportResult) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(result.DataPackagePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read data package: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func assertPathsExist(t *testing.T, paths ...string) {
|
||||
t.Helper()
|
||||
for _, path := range paths {
|
||||
|
||||
Reference in New Issue
Block a user