Reuse collected batch data

This commit is contained in:
2026-06-17 16:04:14 +00:00
parent 6f9255105d
commit a9d87bdbaa
2 changed files with 98 additions and 63 deletions

View File

@@ -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) {

View File

@@ -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