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