Reuse collected batch data
This commit is contained in:
@@ -295,22 +295,14 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
|
||||
}
|
||||
for _, planned := range plannedReports {
|
||||
resolved := planned.Resolved
|
||||
item := batchReportResult(resolved)
|
||||
item := batchReportResult(planned)
|
||||
if paths, err := store.Paths(resolved); err == nil {
|
||||
item.DataPackagePath = paths.DataPackage
|
||||
item.PreflightPath = paths.Preflight
|
||||
item.ReportPath = paths.RenderedReport
|
||||
item.MetadataPath = paths.Metadata
|
||||
}
|
||||
collection, err := collectWeather(ctx, req.Config, req.Collector)
|
||||
if err != nil {
|
||||
item.Status = "failed"
|
||||
item.Error = err.Error()
|
||||
result.Failed++
|
||||
result.Reports = append(result.Reports, item)
|
||||
continue
|
||||
}
|
||||
outputPath := batchOutputPath(req.OutputDir, resolved.Definition)
|
||||
outputPath := plannedBatchOutputPath(req.OutputDir, planned)
|
||||
reportResult, err := GenerateReport(ctx, ReportRequest{
|
||||
Config: req.Config,
|
||||
Resolved: resolved,
|
||||
@@ -357,7 +349,8 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
|
||||
return nil, fmt.Errorf("run is not implemented")
|
||||
}
|
||||
|
||||
func batchReportResult(resolved report.Resolved) BatchReportResult {
|
||||
func batchReportResult(planned plannedBatchReport) BatchReportResult {
|
||||
resolved := planned.Resolved
|
||||
metadata := resolved.Metadata()
|
||||
return BatchReportResult{
|
||||
ReportID: resolved.Definition.ID,
|
||||
@@ -369,11 +362,18 @@ func batchReportResult(resolved report.Resolved) BatchReportResult {
|
||||
}
|
||||
}
|
||||
|
||||
func batchOutputPath(outputDir string, definition report.Definition) string {
|
||||
if outputDir == "" || definition.BatchOutputName == "" {
|
||||
func plannedBatchOutputPath(outputDir string, planned plannedBatchReport) string {
|
||||
if outputDir == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(outputDir, definition.BatchOutputName)
|
||||
outputCopyName := planned.OutputCopyName
|
||||
if outputCopyName == "" {
|
||||
outputCopyName = planned.Resolved.Definition.BatchOutputName
|
||||
}
|
||||
if outputCopyName == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(outputDir, outputCopyName)
|
||||
}
|
||||
|
||||
func ResolveGenerate(req GenerateRequest, now time.Time) (report.Resolved, error) {
|
||||
|
||||
@@ -2236,32 +2236,43 @@ func TestResolveBatchMorningSkipsWeekendOnSunday(t *testing.T) {
|
||||
func TestRunBatchContinuesAfterReportFailure(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
collection := collectionWithFutureDailyForTest(t, cfg, "2026-05-31")
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
collector := &recordingCollector{result: &collection}
|
||||
renderer := &selectiveRenderer{
|
||||
failRenderPrompt: "weather.tomorrow_generated_text",
|
||||
runBody: "# Batch Report\n",
|
||||
}
|
||||
|
||||
result, err := RunBatchDetailed(context.Background(), BatchRequest{
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Renderer: renderer,
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
Renderer: renderer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunBatchDetailed() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Total != 2 || result.Succeeded != 1 || result.Failed != 1 {
|
||||
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 2/1/1", result.Total, result.Succeeded, result.Failed)
|
||||
if result.Total != 3 || result.Succeeded != 2 || result.Failed != 1 {
|
||||
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 3/2/1", result.Total, result.Succeeded, result.Failed)
|
||||
}
|
||||
if renderer.runCalls != 0 || renderer.structuredRunCalls != 1 {
|
||||
if len(collector.requests) != 1 {
|
||||
t.Fatalf("collector requests = %d, want one collection for batch", len(collector.requests))
|
||||
}
|
||||
if renderer.runCalls != 0 || renderer.structuredRunCalls != 2 {
|
||||
t.Fatalf("renderer calls run=%d structured=%d, want successful reports to continue", renderer.runCalls, renderer.structuredRunCalls)
|
||||
}
|
||||
var failedTomorrow bool
|
||||
var succeededDaily bool
|
||||
for _, item := range result.Reports {
|
||||
if item.ReportID == report.Tomorrow && item.Status == "failed" && strings.Contains(item.Error, "render failed") {
|
||||
failedTomorrow = true
|
||||
}
|
||||
if item.ReportID == report.Daily && item.Status == "succeeded" {
|
||||
succeededDaily = true
|
||||
}
|
||||
if item.ReportID != report.Tomorrow && item.Status != "succeeded" {
|
||||
t.Fatalf("report %s status = %s, want succeeded", item.ReportID, item.Status)
|
||||
}
|
||||
@@ -2269,11 +2280,17 @@ func TestRunBatchContinuesAfterReportFailure(t *testing.T) {
|
||||
if !failedTomorrow {
|
||||
t.Fatalf("reports = %#v, want failed Tomorrow item", result.Reports)
|
||||
}
|
||||
if !succeededDaily {
|
||||
t.Fatalf("reports = %#v, want Daily report to continue after Tomorrow failure", result.Reports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyNotificationConfig(t, server)
|
||||
collection := collectionWithFutureDailyForTest(t, cfg, "2026-05-31")
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
collector := &recordingCollector{result: &collection}
|
||||
notifier := &recordingNotifier{
|
||||
errByReport: map[report.ID]error{
|
||||
report.Tomorrow: errors.New("distributor unavailable"),
|
||||
@@ -2281,20 +2298,24 @@ func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
result, err := RunBatchDetailed(context.Background(), BatchRequest{
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Renderer: &selectiveRenderer{runBody: "# Batch Report\n"},
|
||||
Notifier: notifier,
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
Renderer: &selectiveRenderer{runBody: "# Batch Report\n"},
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunBatchDetailed() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Total != 2 || result.Succeeded != 1 || result.Failed != 1 {
|
||||
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 2/1/1", result.Total, result.Succeeded, result.Failed)
|
||||
if result.Total != 3 || result.Succeeded != 2 || result.Failed != 1 {
|
||||
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 3/2/1", result.Total, result.Succeeded, result.Failed)
|
||||
}
|
||||
if len(notifier.requests) != 2 {
|
||||
if len(collector.requests) != 1 {
|
||||
t.Fatalf("collector requests = %d, want one collection for batch", len(collector.requests))
|
||||
}
|
||||
if len(notifier.requests) != 3 {
|
||||
t.Fatalf("notification requests = %d, want one per generated report", len(notifier.requests))
|
||||
}
|
||||
var failedTomorrow bool
|
||||
@@ -2329,10 +2350,12 @@ func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
collector = &recordingCollector{result: &collection}
|
||||
err = RunBatch(context.Background(), BatchRequest{
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
Renderer: &selectiveRenderer{
|
||||
runBody: "# Batch Report\n",
|
||||
},
|
||||
@@ -2346,7 +2369,7 @@ func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) {
|
||||
if !errors.As(err, &batchErr) {
|
||||
t.Fatalf("RunBatch() error = %T %v, want BatchError", err, err)
|
||||
}
|
||||
if batchErr.Result == nil || batchErr.Result.Failed != 1 || batchErr.Result.Succeeded != 1 {
|
||||
if batchErr.Result == nil || batchErr.Result.Failed != 1 || batchErr.Result.Succeeded != 2 {
|
||||
t.Fatalf("RunBatch() result = %#v, want notification failure aggregate", batchErr.Result)
|
||||
}
|
||||
}
|
||||
@@ -2354,6 +2377,9 @@ func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) {
|
||||
func TestRunBatchUsesOutputDirectory(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
collection := collectionWithFutureDailyForTest(t, cfg, "2026-05-31")
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
collector := &recordingCollector{result: &collection}
|
||||
outputDir := filepath.Join(t.TempDir(), "reports")
|
||||
|
||||
result, err := RunBatchDetailed(context.Background(), BatchRequest{
|
||||
@@ -2361,31 +2387,44 @@ func TestRunBatchUsesOutputDirectory(t *testing.T) {
|
||||
Batch: BatchEvening,
|
||||
Now: mustParse("2026-05-29T18:00:00-05:00"),
|
||||
OutputDir: outputDir,
|
||||
Renderer: &selectiveRenderer{runBody: "# Tomorrow\n"},
|
||||
Collector: collector,
|
||||
Renderer: &selectiveRenderer{runBody: "# Batch Report\n"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunBatchDetailed() error = %v", err)
|
||||
}
|
||||
if result.Failed != 0 || len(result.Reports) != 1 {
|
||||
t.Fatalf("summary = %#v, want one successful report", result)
|
||||
if result.Failed != 0 || len(result.Reports) != 2 {
|
||||
t.Fatalf("summary = %#v, want two successful reports", result)
|
||||
}
|
||||
want := filepath.Join(outputDir, "tomorrow.md")
|
||||
if result.Reports[0].OutputPath != want {
|
||||
t.Fatalf("OutputPath = %q, want %q", result.Reports[0].OutputPath, want)
|
||||
if len(collector.requests) != 1 {
|
||||
t.Fatalf("collector requests = %d, want one collection for evening batch", len(collector.requests))
|
||||
}
|
||||
if _, err := os.Stat(want); err != nil {
|
||||
t.Fatalf("expected output copy %q: %v", want, err)
|
||||
wantByReport := map[report.ID]string{
|
||||
report.Tomorrow: filepath.Join(outputDir, "tomorrow.md"),
|
||||
report.Daily: filepath.Join(outputDir, "daily-2026-05-31.md"),
|
||||
}
|
||||
reportData, err := os.ReadFile(result.Reports[0].ReportPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read managed report: %v", err)
|
||||
}
|
||||
copyData, err := os.ReadFile(want)
|
||||
if err != nil {
|
||||
t.Fatalf("read output copy: %v", err)
|
||||
}
|
||||
if string(copyData) != string(reportData) {
|
||||
t.Fatalf("batch output copy differs from managed report")
|
||||
for _, item := range result.Reports {
|
||||
want := wantByReport[item.ReportID]
|
||||
if want == "" {
|
||||
t.Fatalf("unexpected report item = %#v", item)
|
||||
}
|
||||
if item.OutputPath != want {
|
||||
t.Fatalf("%s OutputPath = %q, want %q", item.ReportID, item.OutputPath, want)
|
||||
}
|
||||
if _, err := os.Stat(want); err != nil {
|
||||
t.Fatalf("expected output copy %q: %v", want, err)
|
||||
}
|
||||
reportData, err := os.ReadFile(item.ReportPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read managed report: %v", err)
|
||||
}
|
||||
copyData, err := os.ReadFile(want)
|
||||
if err != nil {
|
||||
t.Fatalf("read output copy: %v", err)
|
||||
}
|
||||
if string(copyData) != string(reportData) {
|
||||
t.Fatalf("batch output copy differs from managed report for %s", item.ReportID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2429,18 +2468,6 @@ func TestRunBatchMorningUsesTodayOutputName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchOutputPathUsesHourlyOutputName(t *testing.T) {
|
||||
definition, err := report.DefaultRegistry().Lookup(report.Hourly)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(hourly) error = %v", err)
|
||||
}
|
||||
outputDir := filepath.Join(t.TempDir(), "reports")
|
||||
want := filepath.Join(outputDir, "hourly.md")
|
||||
if got := batchOutputPath(outputDir, definition); got != want {
|
||||
t.Fatalf("batchOutputPath() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchDetailedUsesProvidedCollector(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
@@ -2506,6 +2533,14 @@ func collectionForTest(t *testing.T, cfg config.Config) collect.Result {
|
||||
return collect.Result{Bundle: bundle}
|
||||
}
|
||||
|
||||
func collectionWithFutureDailyForTest(t *testing.T, cfg config.Config, date string) collect.Result {
|
||||
t.Helper()
|
||||
collection := collectionForTest(t, cfg)
|
||||
location := mustLoadTestLocation(t, cfg.WeatherAPI.Timezone)
|
||||
collection.Bundle.Hourly.Periods = append(collection.Bundle.Hourly.Periods, fullDayPeriods(t, date, location)...)
|
||||
return collection
|
||||
}
|
||||
|
||||
func resolveGenerateForTest(t *testing.T, cfg config.Config, req GenerateRequest, now string) report.Resolved {
|
||||
t.Helper()
|
||||
req.Config = cfg
|
||||
|
||||
Reference in New Issue
Block a user