From 27929338339314385afa4d9c277d14808ef18fc5 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 15 Jun 2026 20:42:48 +0000 Subject: [PATCH] Add data package export regression coverage --- internal/app/app_test.go | 166 ++++++++++++++++++++++++++++++ internal/briefing/modules_test.go | 37 +++++++ 2 files changed, 203 insertions(+) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 921f827..8aa065f 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -309,6 +309,80 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) { } } +func TestGeneratedTemplateReportsUseRichArtifactsAndCuratedDataPackages(t *testing.T) { + server := dailyBundleServer(t) + tests := []struct { + name string + kind ReportKind + date time.Time + now time.Time + prompt string + }{ + { + name: "today", + kind: ReportToday, + date: mustParse("2026-05-29T12:00:00-05:00"), + now: mustParse("2026-05-29T05:00:00-05:00"), + prompt: "weather.today_generated_text", + }, + { + name: "tomorrow", + kind: ReportTomorrow, + now: mustParse("2026-05-29T18:00:00-05:00"), + prompt: "weather.tomorrow_generated_text", + }, + { + name: "daily", + kind: ReportDaily, + date: mustParse("2026-05-29T12:00:00-05:00"), + now: mustParse("2026-05-29T05:00:00-05:00"), + prompt: "weather.daily_generated_text", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := dailyTestConfig(t, server) + cfg.Workspace.Root = t.TempDir() + resolved, err := ResolveGenerate(GenerateRequest{ + Config: cfg, + Report: tt.kind, + Date: tt.date, + }, tt.now) + if err != nil { + t.Fatalf("ResolveGenerate() error = %v", err) + } + renderer := &recordingRenderer{ + renderResult: &scriptorium.RenderResult{ExitCode: 0}, + structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, + } + + result, err := GenerateReport(context.Background(), ReportRequest{ + Config: cfg, + Resolved: resolved, + Renderer: renderer, + }) + if err != nil { + t.Fatalf("GenerateReport() error = %v", err) + } + if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 { + t.Fatalf("renderer calls render=%d structured=%d run=%d, want generated-template workflow", renderer.renderCalls, renderer.structuredRunCalls, renderer.runCalls) + } + if renderer.renderRequest.PromptID != tt.prompt || renderer.structuredRunRequest.PromptID != tt.prompt { + t.Fatalf("prompt IDs render=%q structured=%q, want %q", renderer.renderRequest.PromptID, renderer.structuredRunRequest.PromptID, tt.prompt) + } + if renderer.renderRequest.DataPackagePath != result.DataPackagePath { + t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath) + } + if renderer.structuredRunRequest.DataPackagePath != result.DataPackagePath { + t.Fatalf("structured run DataPackagePath = %q, want managed path %q", renderer.structuredRunRequest.DataPackagePath, result.DataPackagePath) + } + assertRichPromptHelperArtifacts(t, result) + assertCuratedPromptDataPackage(t, result) + }) + } +} + func TestGenerateReportIncludesSPCConvectivePromptStanzas(t *testing.T) { server := dailyBundleServerWithConvectiveResponse(t, qualifyingConvectiveOutlooksResponse) cfg := dailyTestConfig(t, server) @@ -2655,6 +2729,98 @@ func readDataPackageForTest(t *testing.T, result *ReportResult) []byte { return data } +func assertRichPromptHelperArtifacts(t *testing.T, result *ReportResult) { + t.Helper() + snapshotData, err := os.ReadFile(result.ModuleSnapshotPath) + if err != nil { + t.Fatalf("read module snapshot: %v", err) + } + for _, want := range []string{ + `"condition_text_lower"`, + `"hour_label"`, + `"text_description_lower"`, + `"mention_precipitation"`, + `"temperature_phrase_f"`, + `"dominant_condition_lower"`, + `"dominant_condition_display"`, + `"max_pop_time_label"`, + } { + if !strings.Contains(string(snapshotData), want) { + t.Fatalf("module snapshot missing rich helper field %q:\n%s", want, string(snapshotData)) + } + } + + renderContext, err := os.ReadFile(result.RenderContextPath) + if err != nil { + t.Fatalf("read render context: %v", err) + } + for _, want := range []string{ + `"condition_text_lower"`, + `"hour_label"`, + `"text_description_lower"`, + `"mention_precipitation"`, + `"temperature_phrase_f"`, + `"dominant_condition_lower"`, + `"dominant_condition_display"`, + `"max_pop_time_label"`, + } { + if !strings.Contains(string(renderContext), want) { + t.Fatalf("render context missing rich helper field %q:\n%s", want, string(renderContext)) + } + } +} + +func assertCuratedPromptDataPackage(t *testing.T, result *ReportResult) { + t.Helper() + data := readDataPackageForTest(t, result) + pkg, err := promptinput.LoadYAML(data) + if err != nil { + t.Fatalf("decode data package: %v", err) + } + + current, ok := pkg.Briefing.Values["current_conditions"].(map[string]any) + if !ok { + t.Fatalf("current_conditions = %#v, want prompt map", pkg.Briefing.Values["current_conditions"]) + } + assertMapOmitsKeys(t, "current_conditions", current, "condition_text_lower", "wind_direction_text") + + hourly, ok := pkg.Briefing.Values["hourly_forecast"].(map[string]any) + if !ok { + t.Fatalf("hourly_forecast = %#v, want prompt map", pkg.Briefing.Values["hourly_forecast"]) + } + periods, ok := hourly["periods"].([]any) + if !ok || len(periods) == 0 { + t.Fatalf("hourly_forecast.periods = %#v, want prompt periods", hourly["periods"]) + } + firstPeriod, ok := periods[0].(map[string]any) + if !ok { + t.Fatalf("hourly first period = %#v, want prompt map", periods[0]) + } + assertMapOmitsKeys(t, "hourly_forecast.periods[0]", firstPeriod, "hour_label", "text_description_lower", "mention_precipitation") + + dayparts, ok := pkg.Briefing.Values["derived_daypart_summaries"].(map[string]any) + if !ok { + t.Fatalf("derived_daypart_summaries = %#v, want prompt map", pkg.Briefing.Values["derived_daypart_summaries"]) + } + morning, ok := dayparts["morning"].(map[string]any) + if !ok { + t.Fatalf("derived_daypart_summaries.morning = %#v, want prompt map", dayparts["morning"]) + } + if morning["max_pop_time"] != "6:00 AM" { + t.Fatalf("derived_daypart_summaries.morning.max_pop_time = %#v, want friendly label", morning["max_pop_time"]) + } + assertMapOmitsKeys(t, "derived_daypart_summaries.morning", morning, "temperature_phrase_f", "dominant_condition_lower", "dominant_condition_display", "max_pop_time_label") +} + +func assertMapOmitsKeys(t *testing.T, name string, value map[string]any, keys ...string) { + t.Helper() + for _, key := range keys { + if _, ok := value[key]; ok { + t.Fatalf("%s contains helper field %q: %#v", name, key, value) + } + } +} + func assertNoStaleModuleIntervalKeys(t *testing.T, values map[string]any) { t.Helper() for name, value := range values { diff --git a/internal/briefing/modules_test.go b/internal/briefing/modules_test.go index 46f199b..e171e90 100644 --- a/internal/briefing/modules_test.go +++ b/internal/briefing/modules_test.go @@ -59,6 +59,43 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) { } } +func TestDefaultModuleDefinitionsDeclarePromptExportPolicy(t *testing.T) { + customExporters := map[module.ID]struct{}{ + module.CurrentConditions: {}, + module.HourlyForecast: {}, + module.DerivedDaypartSummaries: {}, + } + passThroughExporters := map[module.ID]struct{}{ + module.Metadata: {}, + module.NarrativeForecast: {}, + module.DerivedDailySummary: {}, + module.PrecipTiming: {}, + module.AlertDigest: {}, + module.SPCConvectiveOutlooks: {}, + module.AreaForecastDiscussion: {}, + module.SPCConvectiveDiscussion: {}, + module.WeatherStory: {}, + module.OutdoorWindows: {}, + module.TodayPlanning: {}, + module.TomorrowPlanning: {}, + module.DailyPlanning: {}, + } + + for _, definition := range defaultModuleDefinitions() { + _, custom := customExporters[definition.ID] + _, passThrough := passThroughExporters[definition.ID] + if custom == passThrough { + t.Fatalf("module %q exporter policy custom=%v passThrough=%v, want exactly one policy", definition.ID, custom, passThrough) + } + if custom && definition.PromptExporter == nil { + t.Fatalf("module %q PromptExporter = nil, want custom prompt exporter", definition.ID) + } + if passThrough && definition.PromptExporter != nil { + t.Fatalf("module %q PromptExporter is set, want default pass-through", definition.ID) + } + } +} + func TestModuleRegistryAddsPassThroughPromptValue(t *testing.T) { registry, err := NewModuleRegistry([]ModuleDefinition{ {