diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 8aa065f..71a66fe 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -83,16 +83,11 @@ func TestFetchAndSaveBundleRequiresOutputPath(t *testing.T) { func TestGenerateReportWritesReportAndPreflight(t *testing.T) { server := dailyBundleServer(t) - cfg := dailyTestConfig(t, server) - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ Command: []string{"scriptorium", "render"}, @@ -113,11 +108,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) { runBody: "# Daily Report\n\nRain this morning.\n", } outputPath := filepath.Join(t.TempDir(), "daily.md") - filesystemStore, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - store := &recordingStore{Store: filesystemStore} + store := recordingFilesystemStore(t, cfg) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -342,20 +333,12 @@ func TestGeneratedTemplateReportsUseRichArtifactsAndCuratedDataPackages(t *testi 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, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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}, - } + }, tt.now.Format(time.RFC3339)) + renderer := successfulGeneratedTextRenderer("") result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -477,28 +460,12 @@ func TestGenerateReportOmitsSPCConvectiveDiscussionBelowThreshold(t *testing.T) func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) { server := hourlyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - cfg.Location.ID = "home" - cfg.Location.Name = "Brentwood" - cfg.Location.Region = "MO" - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, - Report: ReportHourly, - }, mustParse("2026-05-29T08:30:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate() error = %v", err) - } + cfg := hourlyTestConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{Report: ReportHourly}, "2026-05-29T08:30:00-05:00") if resolved.Definition.GenerationMode != report.GenerationModeGeneratedTextTemplate { t.Fatalf("GenerationMode = %q, want generated text template", resolved.Definition.GenerationMode) } - filesystemStore, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - store := &recordingStore{Store: filesystemStore} + store := recordingFilesystemStore(t, cfg) renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ Command: []string{"scriptorium", "render"}, @@ -642,18 +609,8 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) { func TestGenerateHourlyReportCopiesOutputAndNotifiesManagedReport(t *testing.T) { cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t) - notifier.result = &NotificationResult{ - RunID: "distributor-run", - Status: "succeeded", - UploadStatus: "accepted", - PipelineID: "reports", - Report: []byte(`{"actions":[{"action":"replace_older"}]}`), - } - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validHourlyGeneratedTextJSON(), - } + notifier.result = successfulNotificationResult() + renderer := successfulGeneratedTextRenderer(validHourlyGeneratedTextJSON()) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -725,38 +682,21 @@ func TestGenerateHourlyReportCopiesOutputAndNotifiesManagedReport(t *testing.T) func TestGenerateTodayReportCopiesOutputAndNotifiesTodayTemplateValues(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() + cfg := dailyWorkspaceConfig(t, server) cfg.Notify.Distributor.Enabled = true cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{report_id}.{artifact_group}" cfg.Notify.Distributor.BundleIDTemplate = "{artifact_group}.{batch_output_name}.{report_id}" cfg.Notify.Distributor.IdempotencyKeyTemplate = "{bundle_id}.{run_id}" cfg.Notify.Distributor.ReportPathTemplates = []string{"{valid_start_date}/{artifact_group}/{batch_output_name}"} - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportToday, 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) - } + }, "2026-05-29T05:00:00-05:00") outputPath := filepath.Join(t.TempDir(), "today-copy.md") notifier := &recordingNotifier{ - result: &NotificationResult{ - RunID: "distributor-run", - Status: "succeeded", - UploadStatus: "accepted", - PipelineID: "reports", - Report: []byte(`{"actions":[{"action":"replace_older"}]}`), - }, - } - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validTodayGeneratedTextJSON(), + result: successfulNotificationResult(), } + renderer := successfulGeneratedTextRenderer(validTodayGeneratedTextJSON()) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -941,28 +881,17 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t func TestGenerateTomorrowReportNotificationUsesTomorrowTemplateValues(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() + cfg := dailyWorkspaceConfig(t, server) cfg.Notify.Distributor.Enabled = true cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{report_id}.{artifact_group}" cfg.Notify.Distributor.BundleIDTemplate = "{artifact_group}.{batch_output_name}.{report_id}" cfg.Notify.Distributor.IdempotencyKeyTemplate = "{bundle_id}.{run_id}" cfg.Notify.Distributor.ReportPathTemplates = []string{"{valid_start_date}/{artifact_group}/{batch_output_name}"} - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportTomorrow, - }, mustParse("2026-05-29T18:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate() error = %v", err) - } + }, "2026-05-29T18:00:00-05:00") notifier := &recordingNotifier{} - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validTomorrowGeneratedTextJSON(), - } + renderer := successfulGeneratedTextRenderer(validTomorrowGeneratedTextJSON()) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -1080,15 +1009,11 @@ func TestGenerateHourlyReportPersistsStructuredRunFailure(t *testing.T) { func TestGenerateHourlyReportPreservesRawTextOnValidationFailure(t *testing.T) { cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t) - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: `{ + renderer := successfulGeneratedTextRenderer(`{ "summary": "Storm chances increase through late morning.", "forecast_discussion": "A front will keep the region unsettled.", "details": "not allowed" - }`, - } + }`) _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -1116,11 +1041,7 @@ func TestGenerateHourlyReportPreservesRawTextOnValidationFailure(t *testing.T) { func TestGenerateHourlyReportRejectsUnsupportedTemplateBeforeStructuredRun(t *testing.T) { cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t) resolved.Definition.TemplateID = "missing-template" - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validHourlyGeneratedTextJSON(), - } + renderer := successfulGeneratedTextRenderer(validHourlyGeneratedTextJSON()) _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -1150,19 +1071,14 @@ func TestGenerateHourlyReportRejectsUnsupportedTemplateBeforeStructuredRun(t *te func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) { server := dailyBundleServer(t) - cfg := dailyTestConfig(t, server) - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") notifier := &recordingNotifier{} - _, err = GenerateReport(context.Background(), ReportRequest{ + _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: resolved, Renderer: successfulRenderer("# Daily Report\n"), @@ -1178,26 +1094,13 @@ func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) { func TestGenerateReportNotifiesManagedReportPath(t *testing.T) { server := dailyBundleServer(t) - cfg := dailyTestConfig(t, server) - cfg.Workspace.Root = t.TempDir() - cfg.Notify.Distributor.Enabled = true - cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyNotificationConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") notifier := &recordingNotifier{ - result: &NotificationResult{ - RunID: "distributor-run", - Status: "succeeded", - UploadStatus: "accepted", - PipelineID: "reports", - Report: []byte(`{"actions":[{"action":"replace_older"}]}`), - }, + result: successfulNotificationResult(), } outputPath := filepath.Join(t.TempDir(), "daily-copy.md") @@ -1266,25 +1169,15 @@ func TestGenerateReportNotifiesManagedReportPath(t *testing.T) { func TestGenerateReportNotificationFailureFailsReport(t *testing.T) { server := dailyBundleServer(t) - cfg := dailyTestConfig(t, server) - cfg.Workspace.Root = t.TempDir() - cfg.Notify.Distributor.Enabled = true - cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyNotificationConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") notifier := &recordingNotifier{err: errors.New("upload rejected")} - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - _, err = GenerateReport(context.Background(), ReportRequest{ + store := recordingFilesystemStore(t, cfg) + _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: resolved, Renderer: successfulRenderer("# Daily Report\n"), @@ -1342,21 +1235,14 @@ func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cfg := dailyTestConfig(t, server) - cfg.Workspace.Root = t.TempDir() - cfg.Notify.Distributor.Enabled = true - cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyNotificationConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") notifier := &recordingNotifier{} - _, err = GenerateReport(context.Background(), ReportRequest{ + _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: resolved, Renderer: tt.renderer, @@ -1379,17 +1265,13 @@ func TestGenerateReportDoesNotNotifyAfterFetchFailure(t *testing.T) { cfg.Workspace.Root = t.TempDir() cfg.Notify.Distributor.Enabled = true cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") notifier := &recordingNotifier{} - _, err = GenerateReport(context.Background(), ReportRequest{ + _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: resolved, Renderer: successfulRenderer("# Daily Report\n"), @@ -1405,18 +1287,11 @@ func TestGenerateReportDoesNotNotifyAfterFetchFailure(t *testing.T) { func TestGenerateReportPersistsFailedPreflight(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ Command: []string{"scriptorium", "render"}, @@ -1426,7 +1301,7 @@ func TestGenerateReportPersistsFailedPreflight(t *testing.T) { err: errors.New("scriptorium render exited with code 1: render failed"), } - _, err = GenerateReport(context.Background(), ReportRequest{ + _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: resolved, Renderer: renderer, @@ -1434,10 +1309,7 @@ func TestGenerateReportPersistsFailedPreflight(t *testing.T) { if err == nil { t.Fatal("GenerateReport() error = nil, want render error") } - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } + store := recordingFilesystemStore(t, cfg) paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) @@ -1460,18 +1332,11 @@ func TestGenerateReportPersistsFailedPreflight(t *testing.T) { func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, structuredRunResult: &scriptorium.StructuredRunResult{ @@ -1481,7 +1346,7 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) { structuredRunErr: errors.New("scriptorium run exited with code 2: validation failed"), } - _, err = GenerateReport(context.Background(), ReportRequest{ + _, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: resolved, Renderer: renderer, @@ -1492,10 +1357,7 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) { if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 { t.Fatalf("calls render=%d structured=%d run=%d, want render and structured run only", renderer.renderCalls, renderer.structuredRunCalls, renderer.runCalls) } - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } + store := recordingFilesystemStore(t, cfg) paths, err := store.Paths(resolved) if err != nil { t.Fatalf("Paths() error = %v", err) @@ -1513,32 +1375,18 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) { func TestGenerateReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportDaily, Date: mustParse("2026-05-29T12:00:00-05:00"), - }, mustParse("2026-05-29T04:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } + }, "2026-05-29T04:00:00-05:00") savePriorRun(t, store, priorResolved, priorDailyModuleSnapshot(t, priorResolved)) - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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(current) error = %v", err) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0}, @@ -1568,37 +1416,19 @@ func TestGenerateReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) { func TestGenerateTodayReportUsesTodayIdentityAndRecentChanges(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportToday, Date: mustParse("2026-05-29T12:00:00-05:00"), - }, mustParse("2026-05-29T04:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } + }, "2026-05-29T04:00:00-05:00") savePriorRun(t, store, priorResolved, priorDailyModuleSnapshot(t, priorResolved)) - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportToday, Date: mustParse("2026-05-29T12:00:00-05:00"), - }, mustParse("2026-05-29T05:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(current) error = %v", err) - } - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validTodayGeneratedTextJSON(), - } + }, "2026-05-29T05:00:00-05:00") + renderer := successfulGeneratedTextRenderer(validTodayGeneratedTextJSON()) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -1655,22 +1485,11 @@ func TestGenerateTodayReportUsesTodayIdentityAndRecentChanges(t *testing.T) { func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportTomorrow, - }, mustParse("2026-05-29T18:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate() error = %v", err) - } - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validTomorrowGeneratedTextJSON(), - } + }, "2026-05-29T18:00:00-05:00") + renderer := successfulGeneratedTextRenderer(validTomorrowGeneratedTextJSON()) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -1732,35 +1551,17 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) { func TestTomorrowReportCanCompareAgainstPriorTomorrowSnapshot(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportTomorrow, - }, mustParse("2026-05-29T17:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } + }, "2026-05-29T17:00:00-05:00") savePriorRun(t, store, priorResolved, priorDailyModuleSnapshot(t, priorResolved)) - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportTomorrow, - }, mustParse("2026-05-29T18:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(current) error = %v", err) - } - renderer := &recordingRenderer{ - renderResult: &scriptorium.RenderResult{ExitCode: 0}, - structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, - structuredRunBody: validTomorrowGeneratedTextJSON(), - } + }, "2026-05-29T18:00:00-05:00") + renderer := successfulGeneratedTextRenderer(validTomorrowGeneratedTextJSON()) result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, @@ -1781,31 +1582,17 @@ func TestTomorrowReportCanCompareAgainstPriorTomorrowSnapshot(t *testing.T) { func TestDailyReportIgnoresPriorTomorrowSnapshot(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportTomorrow, - }, mustParse("2026-05-28T18:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } + }, "2026-05-28T18:00:00-05:00") savePriorRun(t, store, priorResolved, priorDailyModuleSnapshot(t, priorResolved)) - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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(current) error = %v", err) - } + }, "2026-05-29T05:00:00-05:00") result, err := GenerateReport(context.Background(), ReportRequest{ Config: cfg, Resolved: currentResolved, @@ -1825,29 +1612,15 @@ func TestDailyReportIgnoresPriorTomorrowSnapshot(t *testing.T) { func TestGenerateThreeDayReportWritesReportAndRecentChanges(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportThreeDay, - }, mustParse("2026-05-29T04:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } + }, "2026-05-29T04:00:00-05:00") savePriorRun(t, store, priorResolved, priorOutlookModuleSnapshot(t, "2026-05-30")) - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportThreeDay, - }, mustParse("2026-05-29T05:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(current) error = %v", err) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0}, @@ -1884,29 +1657,15 @@ func TestGenerateThreeDayReportWritesReportAndRecentChanges(t *testing.T) { func TestGenerateWeekendReportWritesReportAndRecentChanges(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportWeekend, - }, mustParse("2026-05-29T04:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } + }, "2026-05-29T04:00:00-05:00") savePriorRun(t, store, priorResolved, priorOutlookModuleSnapshot(t, "2026-05-30")) - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportWeekend, - }, mustParse("2026-05-29T05:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(current) error = %v", err) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0}, @@ -1943,19 +1702,12 @@ func TestGenerateWeekendReportWritesReportAndRecentChanges(t *testing.T) { func TestGenerateStormReportWritesReport(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportStorm, StormStart: mustParse("2026-05-29T06:00:00-05:00"), StormEnd: mustParse("2026-05-29T10:00:00-05:00"), - }, mustParse("2026-05-29T05:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate() error = %v", err) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0}, @@ -1993,18 +1745,11 @@ func TestGenerateStormReportWritesReport(t *testing.T) { func TestInspectGeneratedReportArtifacts(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0}, @@ -2061,30 +1806,16 @@ func TestInspectGeneratedReportArtifacts(t *testing.T) { func TestInspectPriorSnapshot(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - store, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - priorResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + cfg := dailyWorkspaceConfig(t, server) + store := recordingFilesystemStore(t, cfg) + priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ Report: ReportDaily, Date: mustParse("2026-05-29T12:00:00-05:00"), - }, mustParse("2026-05-29T04:00:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate(prior) error = %v", err) - } - currentResolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, + }, "2026-05-29T04:00:00-05:00") + currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{ 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(current) error = %v", err) - } + }, "2026-05-29T05:00:00-05:00") renderer := &recordingRenderer{ renderResult: &scriptorium.RenderResult{ExitCode: 0}, runResult: &scriptorium.RunResult{ExitCode: 0}, @@ -2381,10 +2112,7 @@ func TestResolveBatchMorningSkipsWeekendOnSunday(t *testing.T) { func TestRunBatchContinuesAfterReportFailure(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() + cfg := dailyWorkspaceConfig(t, server) renderer := &selectiveRenderer{ failRenderPrompt: "weather.three_day_outlook", runBody: "# Batch Report\n", @@ -2422,12 +2150,7 @@ func TestRunBatchContinuesAfterReportFailure(t *testing.T) { func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - cfg.Notify.Distributor.Enabled = true - cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" + cfg := dailyNotificationConfig(t, server) notifier := &recordingNotifier{ errByReport: map[report.ID]error{ report.ThreeDay: errors.New("distributor unavailable"), @@ -2507,10 +2230,7 @@ func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) { func TestRunBatchUsesOutputDirectory(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() + cfg := dailyWorkspaceConfig(t, server) outputDir := filepath.Join(t.TempDir(), "reports") result, err := RunBatchDetailed(context.Background(), BatchRequest{ @@ -2548,10 +2268,7 @@ func TestRunBatchUsesOutputDirectory(t *testing.T) { func TestRunBatchMorningUsesTodayOutputName(t *testing.T) { server := dailyBundleServer(t) - cfg := config.Defaults() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() + cfg := dailyWorkspaceConfig(t, server) outputDir := filepath.Join(t.TempDir(), "reports") result, err := RunBatchDetailed(context.Background(), BatchRequest{ @@ -2617,6 +2334,70 @@ func dailyTestConfig(t *testing.T, server *httptest.Server) config.Config { return cfg } +func dailyWorkspaceConfig(t *testing.T, server *httptest.Server) config.Config { + t.Helper() + cfg := dailyTestConfig(t, server) + cfg.Workspace.Root = t.TempDir() + return cfg +} + +func dailyNotificationConfig(t *testing.T, server *httptest.Server) config.Config { + t.Helper() + cfg := dailyWorkspaceConfig(t, server) + cfg.Notify.Distributor.Enabled = true + cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" + return cfg +} + +func resolveGenerateForTest(t *testing.T, cfg config.Config, req GenerateRequest, now string) report.Resolved { + t.Helper() + req.Config = cfg + resolved, err := ResolveGenerate(req, mustParse(now)) + if err != nil { + t.Fatalf("ResolveGenerate() error = %v", err) + } + return resolved +} + +func recordingFilesystemStore(t *testing.T, cfg config.Config) *recordingStore { + t.Helper() + filesystemStore, err := state.NewFilesystemStore(cfg.Workspace) + if err != nil { + t.Fatalf("NewFilesystemStore() error = %v", err) + } + return &recordingStore{Store: filesystemStore} +} + +func successfulNotificationResult() *NotificationResult { + return &NotificationResult{ + RunID: "distributor-run", + Status: "succeeded", + UploadStatus: "accepted", + PipelineID: "reports", + Report: []byte(`{"actions":[{"action":"replace_older"}]}`), + } +} + +func successfulGeneratedTextRenderer(body string) *recordingRenderer { + return &recordingRenderer{ + renderResult: &scriptorium.RenderResult{ExitCode: 0}, + structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0}, + structuredRunBody: body, + } +} + +func hourlyTestConfig(t *testing.T, server *httptest.Server) config.Config { + t.Helper() + cfg := config.Defaults() + cfg.WeatherAPI.BaseURL = server.URL + "/" + cfg.WeatherAPI.Timezone = "America/Chicago" + cfg.Workspace.Root = t.TempDir() + cfg.Location.ID = "home" + cfg.Location.Name = "Brentwood" + cfg.Location.Region = "MO" + return cfg +} + func hourlyGeneratedTextFixture(t *testing.T) (config.Config, report.Resolved, *recordingStore, *recordingNotifier, string) { t.Helper() server := hourlyBundleServer(t) @@ -2653,12 +2434,10 @@ func hourlyGeneratedTextConfigWithModules(t *testing.T, server *httptest.Server, func applyHourlyGeneratedTextSettings(cfg *config.Config, t *testing.T, server *httptest.Server) { t.Helper() - cfg.WeatherAPI.BaseURL = server.URL + "/" - cfg.WeatherAPI.Timezone = "America/Chicago" - cfg.Workspace.Root = t.TempDir() - cfg.Location.ID = "home" - cfg.Location.Name = "Brentwood" - cfg.Location.Region = "MO" + base := hourlyTestConfig(t, server) + cfg.WeatherAPI = base.WeatherAPI + cfg.Workspace = base.Workspace + cfg.Location = base.Location cfg.Notify.Distributor.Enabled = true cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{artifact_group}" cfg.Notify.Distributor.BundleIDTemplate = "weatherreporter.{location_id}.{report_id}" @@ -2668,18 +2447,8 @@ func applyHourlyGeneratedTextSettings(cfg *config.Config, t *testing.T, server * func resolveHourlyGeneratedTextFixture(t *testing.T, cfg config.Config) (report.Resolved, *recordingStore, *recordingNotifier, string) { t.Helper() - resolved, err := ResolveGenerate(GenerateRequest{ - Config: cfg, - Report: ReportHourly, - }, mustParse("2026-05-29T08:30:00-05:00")) - if err != nil { - t.Fatalf("ResolveGenerate() error = %v", err) - } - filesystemStore, err := state.NewFilesystemStore(cfg.Workspace) - if err != nil { - t.Fatalf("NewFilesystemStore() error = %v", err) - } - return resolved, &recordingStore{Store: filesystemStore}, &recordingNotifier{}, filepath.Join(t.TempDir(), "hourly-copy.md") + resolved := resolveGenerateForTest(t, cfg, GenerateRequest{Report: ReportHourly}, "2026-05-29T08:30:00-05:00") + return resolved, recordingFilesystemStore(t, cfg), &recordingNotifier{}, filepath.Join(t.TempDir(), "hourly-copy.md") } func validHourlyGeneratedTextJSON() string {