Preflight batch output destinations

This commit is contained in:
2026-08-01 21:51:42 +00:00
parent 28bdc04fba
commit bf1746a756
4 changed files with 64 additions and 11 deletions

View File

@@ -334,20 +334,19 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
if err != nil {
return nil, err
}
if err := prepareBatchOutputs(req.OutputDir, plannedReports); err != nil {
return nil, err
}
if req.Batch == BatchEvening || req.Batch == BatchMorning {
startedAt := now
result := &BatchResult{Batch: req.Batch, StartedAt: startedAt}
for _, planned := range plannedReports {
resolved := planned.Resolved
item := batchReportResult(planned)
outputPath, err := plannedBatchOutputPath(req.OutputDir, planned)
if err != nil {
return nil, err
}
reportResult, err := generatePromptReport(ctx, promptReportRequest{
GenerateRequest: GenerateRequest{
Config: req.Config,
OutputPath: outputPath,
OutputPath: planned.OutputPath,
Notifier: req.Notifier,
Executor: req.Executor,
},
@@ -451,6 +450,17 @@ func plannedBatchOutputPath(outputDir string, planned plannedBatchReport) (strin
return validateOutputPath(filepath.Join(outputDir, outputName))
}
func prepareBatchOutputs(outputDir string, plannedReports []plannedBatchReport) error {
for index := range plannedReports {
outputPath, err := plannedBatchOutputPath(outputDir, plannedReports[index])
if err != nil {
return err
}
plannedReports[index].OutputPath = outputPath
}
return nil
}
func resolveReportOutputPath(workingDir, override string, resolved report.Resolved) (string, error) {
outputName, err := resolved.OutputName()
if err != nil {

View File

@@ -59,6 +59,42 @@ func TestRunBatchDetailedNotifiesOnlyAfterAllOutputsExist(t *testing.T) {
}
}
func TestRunBatchDetailedPreflightsAllOutputPaths(t *testing.T) {
bundle := generationBundle(t)
bundle.Hourly.Periods = bundle.Hourly.Periods[:1]
outputDir := t.TempDir()
if err := os.Mkdir(filepath.Join(outputDir, "tomorrow.md"), 0o700); err != nil {
t.Fatal(err)
}
todayPath := filepath.Join(outputDir, "today.md")
const previousReport = "previous report"
if err := os.WriteFile(todayPath, []byte(previousReport), 0o600); err != nil {
t.Fatal(err)
}
executor := &generationExecutor{}
promptInspectedBeforeCollection := false
collector := &generationCollector{
bundle: &bundle,
beforeRun: func() {
promptInspectedBeforeCollection = executor.promptInspections > 0
},
}
result, err := RunBatchDetailed(context.Background(), BatchRequest{
Config: generationDistributorConfig(), Batch: BatchMorning,
Now: generationTime("2026-05-29T08:30:00-05:00"), WorkingDir: t.TempDir(), OutputDir: outputDir,
Collector: collector, Executor: executor, Notifier: &generationNotifier{},
})
if err == nil || result != nil || !collector.called || !promptInspectedBeforeCollection || executor.called {
t.Fatalf("RunBatchDetailed() result/error/collection/inspection/execution = %#v/%v/%t/%t/%t", result, err, collector.called, promptInspectedBeforeCollection, executor.called)
}
if data, readErr := os.ReadFile(todayPath); readErr != nil || string(data) != previousReport {
t.Fatalf("earlier output = %q, error = %v", data, readErr)
}
if info, statErr := os.Stat(filepath.Join(outputDir, "tomorrow.md")); statErr != nil || !info.IsDir() {
t.Fatalf("blocked output info/error = %#v/%v", info, statErr)
}
}
func generationDistributorConfig() config.Config {
cfg := generationConfig()
cfg.Notify.Distributor.Enabled = true

View File

@@ -11,7 +11,8 @@ import (
)
type plannedBatchReport struct {
Resolved report.Resolved
Resolved report.Resolved
OutputPath string
}
func planBatchRun(req BatchRequest, now time.Time, collection collect.Result) ([]plannedBatchReport, error) {

View File

@@ -17,18 +17,23 @@ import (
)
type generationCollector struct {
bundle *weatherdata.Bundle
err error
called bool
bundle *weatherdata.Bundle
err error
called bool
beforeRun func()
}
func (c *generationCollector) Run(context.Context, collect.Request) (*collect.Result, error) {
if c.beforeRun != nil {
c.beforeRun()
}
c.called = true
return &collect.Result{Bundle: c.bundle}, c.err
}
type generationExecutor struct {
called bool
promptInspections int
inspectErr error
executeErr error
cancelBeforeReturn context.CancelFunc
@@ -37,14 +42,15 @@ type generationExecutor struct {
failedPrompt string
}
func (e generationExecutor) InspectPrompt(_ context.Context, id, version string) (promptexec.PromptInspection, error) {
func (e *generationExecutor) InspectPrompt(_ context.Context, id, version string) (promptexec.PromptInspection, error) {
e.promptInspections++
if e.inspectErr != nil {
return promptexec.PromptInspection{}, e.inspectErr
}
definition := generationDefinitionForPrompt(id)
return promptexec.PromptInspection{PromptID: id, PromptVersion: version, PromptHash: "prompt-hash", DefaultProfileID: "fixture", Inputs: []promptexec.InputDefinition{{Name: "data_package", Required: true, ContentType: "application/yaml"}}, Output: promptexec.OutputContract{Format: "json", ValidationMode: "json_schema", SchemaPath: definition.GeneratedTextSchemaID + ".generated_text.schema.json"}}, nil
}
func (generationExecutor) InspectProfile(_ context.Context, id string) (promptexec.ProfileInspection, error) {
func (*generationExecutor) InspectProfile(_ context.Context, id string) (promptexec.ProfileInspection, error) {
return promptexec.ProfileInspection{ProfileID: id, BackendID: "fixture", ModelName: "fixture-model"}, nil
}
func (e *generationExecutor) Execute(_ context.Context, req promptexec.ExecuteRequest, callback promptexec.PreparationCallback) (*promptexec.Execution, error) {