diff --git a/internal/app/app.go b/internal/app/app.go index c01aee8..8d31818 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -94,7 +94,6 @@ type ReportResult struct { LLMDebugPath string ReportPath string OutputPath string - NotificationPath string Metadata state.Metadata MetadataPath string GeneratedTextRawPath string @@ -121,7 +120,6 @@ type BatchNotificationResult struct { PipelineID string `json:"pipelineId,omitempty"` BundleID string `json:"bundleId,omitempty"` IdempotencyKey string `json:"idempotencyKey,omitempty"` - Path string `json:"path,omitempty"` IncludedReports []BatchNotificationReport `json:"includedReports,omitempty"` Error string `json:"error,omitempty"` } @@ -144,7 +142,6 @@ type BatchReportResult struct { NotificationRunID string `json:"notificationRunId,omitempty"` NotificationPipelineID string `json:"notificationPipelineId,omitempty"` NotificationError string `json:"notificationError,omitempty"` - NotificationPath string `json:"notificationPath,omitempty"` GeneratedAt time.Time `json:"generatedAt"` ValidPeriod timeutil.Period `json:"validPeriod"` DataPackagePath string `json:"dataPackagePath,omitempty"` @@ -387,7 +384,7 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro result.Reports = append(result.Reports, item) } result.Total = len(result.Reports) - batchNotification, err := notifyBatch(ctx, req.Config, req.Batch, batchRunID(startedAt, req.Batch), startedAt, result, plannedReports, store, req.Notifier) + batchNotification, err := notifyBatch(ctx, req.Config, req.Batch, batchRunID(startedAt, req.Batch), startedAt, result, plannedReports, req.Notifier) if batchNotification != nil { result.Notification = batchNotification } @@ -408,7 +405,6 @@ func copyBatchReportPaths(item *BatchReportResult, result *ReportResult) { item.ReportPath = result.ReportPath item.OutputPath = result.OutputPath item.MetadataPath = result.MetadataPath - item.NotificationPath = result.NotificationPath if result.Notification != nil { item.NotificationStatus = result.Notification.Status item.NotificationRunID = result.Notification.RunID @@ -617,33 +613,32 @@ func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*weatherda } type finalizeRenderedReportRequest struct { - Config config.Config - Store state.Store - Resolved report.Resolved - Metadata state.Metadata - MetadataPath string - ExecutionArtifact *state.PromptExecutionArtifact - ManagedReportPath string - OutputPath string - Notifier Notifier - GenerationErr error - noNotify bool + Config config.Config + Store state.Store + Resolved report.Resolved + Metadata state.Metadata + MetadataPath string + ExecutionArtifact *state.PromptExecutionArtifact + RenderedReportPath string + OutputPath string + Notifier Notifier + GenerationErr error + noNotify bool } type finalizeRenderedReportResult struct { - OutputPath string - NotificationPath string - Metadata state.Metadata - MetadataPath string - Notification *NotificationResult + OutputPath string + Metadata state.Metadata + MetadataPath string + Notification *NotificationResult } func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportRequest) (finalizeRenderedReportResult, error) { if req.Store == nil { return finalizeRenderedReportResult{}, fmt.Errorf("state store is required") } - if req.ManagedReportPath == "" { - return finalizeRenderedReportResult{}, fmt.Errorf("managed report path is required for report %q", req.Resolved.Definition.ID) + if req.RenderedReportPath == "" { + return finalizeRenderedReportResult{}, fmt.Errorf("rendered report path is required for report %q", req.Resolved.Definition.ID) } if req.ExecutionArtifact == nil { return finalizeRenderedReportResult{}, fmt.Errorf("prompt execution artifact is required for report %q", req.Resolved.Definition.ID) @@ -651,8 +646,8 @@ func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportReque result := finalizeRenderedReportResult{Metadata: req.Metadata, MetadataPath: req.MetadataPath} if req.OutputPath != "" && req.GenerationErr == nil { - if req.OutputPath != req.ManagedReportPath { - if err := fileutil.CopyFileAtomic(req.ManagedReportPath, req.OutputPath); err != nil { + if req.OutputPath != req.RenderedReportPath { + if err := fileutil.CopyFileAtomic(req.RenderedReportPath, req.OutputPath); err != nil { return result, err } result.OutputPath = req.OutputPath @@ -667,7 +662,7 @@ func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportReque } metadata := req.Metadata - metadata.RenderedReportPath = req.ManagedReportPath + metadata.RenderedReportPath = req.RenderedReportPath metadataPath, err := req.Store.SaveMetadata(ctx, metadata) if err != nil { return result, err @@ -681,24 +676,7 @@ func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportReque return result, nil } - notification, notificationPath, err := notifyReport(ctx, req.Config, req.Resolved, req.OutputPath, metadata, req.Notifier, req.Store) - if notificationPath != "" { - result.NotificationPath = notificationPath - result.Notification = notification - metadata.NotificationPath = notificationPath - result.Metadata = metadata - if saveErr := persistReachedPromptPath(ctx, req.Store, req.Resolved, req.ExecutionArtifact, func(paths *state.PromptExecutionPaths) { - paths.NotificationPath = notificationPath - }); saveErr != nil { - return result, saveErr - } - metadataPath, saveErr := req.Store.SaveMetadata(ctx, metadata) - if saveErr != nil { - return result, saveErr - } - result.Metadata = metadata - result.MetadataPath = metadataPath - } + notification, err := notifyReport(ctx, req.Config, req.Resolved, req.OutputPath, metadata.RunID, metadata.GeneratedAt, req.Notifier) result.Notification = notification if err != nil { return result, err @@ -706,31 +684,23 @@ func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportReque return result, nil } -func notifyReport(ctx context.Context, cfg config.Config, resolved report.Resolved, reportPath string, metadata state.Metadata, notifier Notifier, store state.Store) (*NotificationResult, string, error) { +func notifyReport(ctx context.Context, cfg config.Config, resolved report.Resolved, outputPath, runID string, generatedAt time.Time, notifier Notifier) (*NotificationResult, error) { notifier, enabled := reportNotifier(cfg, notifier) if !enabled { - return nil, "", nil + return nil, nil } - notificationRequest, err := buildNotificationRequest(cfg, resolved, reportPath, metadata) + notificationRequest, err := buildNotificationRequest(cfg, resolved, outputPath, runID, generatedAt) if err != nil { - notificationPath, saveErr := saveNotificationArtifact(ctx, store, resolved, cfg, metadata, NotificationRequest{}, nil, err) - if saveErr != nil { - return nil, "", saveErr - } - return nil, notificationPath, err + return nil, err } result, err := notifier.Notify(ctx, notificationRequest) - notificationPath, saveErr := saveNotificationArtifact(ctx, store, resolved, cfg, metadata, notificationRequest, result, err) - if saveErr != nil { - return nil, "", saveErr - } if err != nil { - return result, notificationPath, &NotificationError{ + return result, &NotificationError{ Request: notificationRequest, - Err: fmt.Errorf("notify report %q run %q from output %q: %w", resolved.Definition.ID, metadata.RunID, reportPath, err), + Err: fmt.Errorf("notify report %q run %q from output %q: %w", resolved.Definition.ID, runID, outputPath, err), } } - return result, notificationPath, nil + return result, nil } func reportNotifier(cfg config.Config, notifier Notifier) (Notifier, bool) { @@ -745,8 +715,8 @@ func reportNotifier(cfg config.Config, notifier Notifier) (Notifier, bool) { }, true } -func buildNotificationRequest(cfg config.Config, resolved report.Resolved, reportPath string, metadata state.Metadata) (NotificationRequest, error) { - values, err := distributorTemplateValuesForReport(cfg, resolved, metadata.RunID, filepath.Base(reportPath)) +func buildNotificationRequest(cfg config.Config, resolved report.Resolved, outputPath, runID string, generatedAt time.Time) (NotificationRequest, error) { + values, err := distributorTemplateValuesForReport(cfg, resolved, runID, filepath.Base(outputPath)) if err != nil { return NotificationRequest{}, err } @@ -763,19 +733,19 @@ func buildNotificationRequest(cfg config.Config, resolved report.Resolved, repor if err != nil { return NotificationRequest{}, err } - bundlePaths, err := renderDistributorReportBundlePaths(cfg, resolved, metadata.RunID, reportPath, values) + bundlePaths, err := renderDistributorReportBundlePaths(cfg, resolved, runID, outputPath, values) if err != nil { return NotificationRequest{}, err } return NotificationRequest{ ReportID: resolved.Definition.ID, - RunID: metadata.RunID, + RunID: runID, PipelineID: pipelineID, BundleID: bundleID, IdempotencyKey: idempotencyKey, - ReportPath: reportPath, + ReportPath: outputPath, BundlePaths: bundlePaths, - CreatedAt: metadata.GeneratedAt, + CreatedAt: generatedAt, }, nil } @@ -849,54 +819,6 @@ func addDistributorValidPeriodValues(values *config.DistributorTemplateValues, p return nil } -func saveNotificationArtifact(ctx context.Context, store state.Store, resolved report.Resolved, cfg config.Config, metadata state.Metadata, req NotificationRequest, result *NotificationResult, notifyErr error) (string, error) { - if store == nil { - return "", fmt.Errorf("state store is required") - } - artifact := state.DistributorNotificationArtifact{ - SchemaVersion: state.DistributorNotificationSchemaVersion, - RunID: metadata.RunID, - ReportID: resolved.Definition.ID, - AttemptedAt: time.Now(), - Endpoint: cfg.Notify.Distributor.Endpoint, - PipelineID: req.PipelineID, - BundleID: req.BundleID, - IdempotencyKey: req.IdempotencyKey, - SourcePath: req.ReportPath, - BundlePaths: append([]string(nil), req.BundlePaths...), - BundleCreated: req.CreatedAt, - Status: "attempted", - } - if result != nil { - artifact.Status = result.Status - artifact.Upload = &state.DistributorUploadResult{ - RunID: result.RunID, - Status: result.UploadStatus, - } - if result.PipelineID != "" || !result.AcceptedAt.IsZero() || result.StartedAt != nil || result.FinishedAt != nil || len(result.Report) > 0 || result.Error != "" { - artifact.RunStatus = &state.DistributorRunStatus{ - RunID: result.RunID, - PipelineID: result.PipelineID, - Status: result.Status, - AcceptedAt: result.AcceptedAt, - StartedAt: result.StartedAt, - FinishedAt: result.FinishedAt, - Report: append([]byte(nil), result.Report...), - Error: result.Error, - } - } - artifact.StatusError = result.StatusError - } - if notifyErr != nil { - artifact.Status = "failed" - artifact.Error = notifyErr.Error() - } - if artifact.Status == "" { - artifact.Status = "unknown" - } - return store.SaveDistributorNotification(ctx, resolved, artifact) -} - type noopNotifier struct{} func (noopNotifier) Notify(context.Context, NotificationRequest) (*NotificationResult, error) { diff --git a/internal/app/batch_notification.go b/internal/app/batch_notification.go index b1b8335..11a6ef3 100644 --- a/internal/app/batch_notification.go +++ b/internal/app/batch_notification.go @@ -9,7 +9,6 @@ import ( distributoradapter "gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/distributor" "gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" - "gitea.maximumdirect.net/eric/weatherreporter/internal/state" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) @@ -47,7 +46,7 @@ func batchRunID(startedAt time.Time, batch BatchKind) string { return startedAt.UTC().Format(runIDTimestampLayout) + "_" + string(batch) } -func notifyBatch(ctx context.Context, cfg config.Config, batch BatchKind, runID string, startedAt time.Time, result *BatchResult, planned []plannedBatchReport, store state.Store, notifier Notifier) (*BatchNotificationResult, error) { +func notifyBatch(ctx context.Context, cfg config.Config, batch BatchKind, runID string, startedAt time.Time, result *BatchResult, planned []plannedBatchReport, notifier Notifier) (*BatchNotificationResult, error) { if !cfg.Notify.Distributor.Enabled { return nil, nil } @@ -66,20 +65,12 @@ func notifyBatch(ctx context.Context, cfg config.Config, batch BatchKind, runID req, err := buildBatchNotificationRequest(cfg, batch, runID, startedAt, result.Reports, planned) if err != nil { - path, saveErr := saveBatchNotificationArtifact(ctx, store, cfg, batch, runID, startedAt, batchNotificationRequest{}, nil, err) - if saveErr != nil { - return nil, saveErr - } - return failedBatchNotificationResult(batchNotificationRequest{}, path, err), err + return failedBatchNotificationResult(batchNotificationRequest{}, err), err } batchNotifier, err := resolveBatchNotifier(cfg, notifier) if err != nil { - path, saveErr := saveBatchNotificationArtifact(ctx, store, cfg, batch, runID, startedAt, req, nil, err) - if saveErr != nil { - return nil, saveErr - } - return failedBatchNotificationResult(req, path, err), err + return failedBatchNotificationResult(req, err), err } notification, notifyErr := batchNotifier.NotifyBatch(ctx, req) @@ -87,12 +78,7 @@ func notifyBatch(ctx context.Context, cfg config.Config, batch BatchKind, runID if notifyErr != nil { wrappedErr = fmt.Errorf("notify batch %q run %q bundle %q: %w", batch, runID, req.BundleID, notifyErr) } - path, saveErr := saveBatchNotificationArtifact(ctx, store, cfg, batch, runID, startedAt, req, notification, wrappedErr) - if saveErr != nil { - return nil, saveErr - } - - batchResult := batchNotificationResult(req, notification, path) + batchResult := batchNotificationResult(req, notification) if wrappedErr != nil { batchResult.Status = "failed" batchResult.Error = wrappedErr.Error() @@ -226,13 +212,12 @@ func batchDistributorUploadRequest(req batchNotificationRequest) distributoradap } } -func batchNotificationResult(req batchNotificationRequest, result *NotificationResult, path string) *BatchNotificationResult { +func batchNotificationResult(req batchNotificationRequest, result *NotificationResult) *BatchNotificationResult { notification := &BatchNotificationResult{ Status: "unknown", PipelineID: req.PipelineID, BundleID: req.BundleID, IdempotencyKey: req.IdempotencyKey, - Path: path, IncludedReports: append([]BatchNotificationReport(nil), req.IncludedReports...), } if result != nil { @@ -257,8 +242,8 @@ func batchNotificationResult(req batchNotificationRequest, result *NotificationR return notification } -func failedBatchNotificationResult(req batchNotificationRequest, path string, err error) *BatchNotificationResult { - notification := batchNotificationResult(req, nil, path) +func failedBatchNotificationResult(req batchNotificationRequest, err error) *BatchNotificationResult { + notification := batchNotificationResult(req, nil) notification.Status = "failed" if err != nil { notification.Error = err.Error() @@ -266,78 +251,6 @@ func failedBatchNotificationResult(req batchNotificationRequest, path string, er return notification } -func saveBatchNotificationArtifact(ctx context.Context, store state.Store, cfg config.Config, batch BatchKind, runID string, startedAt time.Time, req batchNotificationRequest, result *NotificationResult, notifyErr error) (string, error) { - if store == nil { - return "", fmt.Errorf("state store is required") - } - location, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone) - if err != nil { - return "", fmt.Errorf("load batch notification timezone: %w", err) - } - artifact := state.BatchDistributorNotificationArtifact{ - SchemaVersion: state.BatchDistributorNotificationSchemaVersion, - Batch: string(batch), - BatchRunID: runID, - AttemptedAt: time.Now(), - Endpoint: cfg.Notify.Distributor.Endpoint, - PipelineID: req.PipelineID, - BundleID: req.BundleID, - IdempotencyKey: req.IdempotencyKey, - BundleCreated: req.CreatedAt, - Reports: batchNotificationReportArtifacts(req.IncludedReports), - Status: "attempted", - } - if result != nil { - artifact.Status = result.Status - artifact.Upload = &state.DistributorUploadResult{ - RunID: result.RunID, - Status: result.UploadStatus, - } - if result.PipelineID != "" || !result.AcceptedAt.IsZero() || result.StartedAt != nil || result.FinishedAt != nil || len(result.Report) > 0 || result.Error != "" { - artifact.RunStatus = &state.DistributorRunStatus{ - RunID: result.RunID, - PipelineID: result.PipelineID, - Status: result.Status, - AcceptedAt: result.AcceptedAt, - StartedAt: result.StartedAt, - FinishedAt: result.FinishedAt, - Report: append([]byte(nil), result.Report...), - Error: result.Error, - } - } - artifact.StatusError = result.StatusError - } - if notifyErr != nil { - artifact.Status = "failed" - artifact.Error = notifyErr.Error() - } - if artifact.Status == "" { - artifact.Status = "unknown" - } - return store.SaveBatchDistributorNotification(ctx, state.BatchDistributorNotificationRef{ - Batch: string(batch), - BatchRunID: runID, - StartedAt: startedAt, - Location: location, - }, artifact) -} - -func batchNotificationReportArtifacts(reports []BatchNotificationReport) []state.BatchDistributorNotificationReportArtifact { - if len(reports) == 0 { - return nil - } - artifacts := make([]state.BatchDistributorNotificationReportArtifact, 0, len(reports)) - for _, item := range reports { - artifacts = append(artifacts, state.BatchDistributorNotificationReportArtifact{ - ReportID: item.ReportID, - RunID: item.RunID, - SourcePath: item.SourcePath, - BundlePaths: append([]string(nil), item.BundlePaths...), - }) - } - return artifacts -} - func renderBatchNotificationIdentity(cfg config.Config, batch BatchKind, runID string, startedAt time.Time) (batchNotificationIdentity, error) { values, err := batchNotificationTemplateValues(cfg, batch, runID, startedAt) if err != nil { diff --git a/internal/app/batch_workflow_test.go b/internal/app/batch_workflow_test.go index 6878b7d..e7351b3 100644 --- a/internal/app/batch_workflow_test.go +++ b/internal/app/batch_workflow_test.go @@ -169,12 +169,12 @@ func TestRunBatchDetailedExecutesRetainedReportsSequentially(t *testing.T) { t.Fatalf("output = %q, want %q", item.OutputPath, test.wantCopies[index]) } assertBatchPathsExist(t, item.DataPackagePath, item.PreparationPath, item.ExecutionPath, item.LLMDebugPath, item.ReportPath, item.OutputPath, item.MetadataPath) - managed, readErr := os.ReadFile(item.ReportPath) + renderedReport, readErr := os.ReadFile(item.ReportPath) if readErr != nil { - t.Fatalf("read managed report: %v", readErr) + t.Fatalf("read rendered report: %v", readErr) } copied, readErr := os.ReadFile(item.OutputPath) - if readErr != nil || !bytes.Equal(managed, copied) { + if readErr != nil || !bytes.Equal(renderedReport, copied) { t.Fatalf("output mismatch/error = %v", readErr) } } @@ -263,15 +263,12 @@ func TestRunBatchDetailedNotificationLifecycle(t *testing.T) { if err != nil { t.Fatalf("RunBatchDetailed() error = %v", err) } - if result.Failed != 0 || result.Notification == nil || result.Notification.Status != "succeeded" || result.Notification.Path == "" || len(notifier.reportRequests) != 0 || len(notifier.batchRequests) != 1 { + if result.Failed != 0 || result.Notification == nil || result.Notification.Status != "succeeded" || len(notifier.reportRequests) != 0 || len(notifier.batchRequests) != 1 { t.Fatalf("notification result/requests = %#v/%d/%d", result.Notification, len(notifier.reportRequests), len(notifier.batchRequests)) } outputPaths := make(map[string]struct{}, len(result.Reports)) for _, item := range result.Reports { outputPaths[item.OutputPath] = struct{}{} - if item.NotificationPath != "" { - t.Fatalf("report item contains per-report notification path: %#v", item) - } } request := notifier.batchRequests[0] if len(request.IncludedReports) != len(result.Reports) { @@ -282,10 +279,6 @@ func TestRunBatchDetailedNotificationLifecycle(t *testing.T) { t.Fatalf("notification file = %#v, want selected Markdown output source", file) } } - artifact := readBatchNotificationArtifact(t, result.Notification.Path) - if artifact.Status != "succeeded" || artifact.Upload == nil || artifact.Upload.RunID != "batch-notification-run" || artifact.RunStatus == nil || len(artifact.Reports) != len(result.Reports) { - t.Fatalf("notification artifact = %#v", artifact) - } }) t.Run("upload failure", func(t *testing.T) { @@ -299,7 +292,7 @@ func TestRunBatchDetailedNotificationLifecycle(t *testing.T) { if err != nil { t.Fatalf("RunBatchDetailed() error = %v", err) } - if result.Succeeded != 2 || result.Failed != 1 || result.Notification == nil || result.Notification.Status != "failed" || result.Notification.Path == "" { + if result.Succeeded != 2 || result.Failed != 1 || result.Notification == nil || result.Notification.Status != "failed" { t.Fatalf("result = %#v, want successful reports and failed notification", result) } for _, item := range result.Reports { @@ -307,9 +300,8 @@ func TestRunBatchDetailedNotificationLifecycle(t *testing.T) { t.Fatalf("report item = %#v, want success despite notification failure", item) } } - artifact := readBatchNotificationArtifact(t, result.Notification.Path) - if artifact.Status != "failed" || !strings.Contains(artifact.Error, "batch upload rejected") { - t.Fatalf("notification artifact = %#v", artifact) + if !strings.Contains(result.Notification.Error, "batch upload rejected") { + t.Fatalf("notification error = %#v", result.Notification) } }) @@ -324,12 +316,11 @@ func TestRunBatchDetailedNotificationLifecycle(t *testing.T) { Config: cfg, Batch: BatchEvening, Now: workflowTime("2026-05-29T18:00:00-05:00"), WorkingDir: t.TempDir(), Collector: &workflowCollector{result: &collect.Result{Bundle: &bundle}}, Executor: newAssembledBatchExecutor(), Notifier: notifier, }) - if err != nil || result.Failed != 0 || result.Notification == nil || result.Notification.Path == "" { + if err != nil || result.Failed != 0 || result.Notification == nil { t.Fatalf("result/error = %#v/%v", result, err) } - artifact := readBatchNotificationArtifact(t, result.Notification.Path) - if artifact.StatusError != "status lookup unavailable" || artifact.RunStatus == nil || !bytes.Contains(artifact.RunStatus.Report, []byte("replace_older")) { - t.Fatalf("notification artifact = %#v", artifact) + if result.Notification.Status != "accepted" || result.Notification.RunID != "batch-notification-run" { + t.Fatalf("notification = %#v", result.Notification) } }) } @@ -439,25 +430,11 @@ func assertBatchItemMatchesMetadata(t *testing.T, item BatchReportResult) { } if item.ReportID != metadata.ReportID || item.RunID != metadata.RunID || item.DataPackagePath != metadata.DataPackagePath || item.PreparationPath != metadata.PreparationPath || - item.ExecutionPath != metadata.ExecutionPath || item.ReportPath != metadata.RenderedReportPath || - item.NotificationPath != metadata.NotificationPath { + item.ExecutionPath != metadata.ExecutionPath || item.ReportPath != metadata.RenderedReportPath { t.Fatalf("batch item paths do not exactly match metadata: item=%#v metadata=%#v", item, metadata) } } -func readBatchNotificationArtifact(t *testing.T, path string) state.BatchDistributorNotificationArtifact { - t.Helper() - data, err := os.ReadFile(path) - if err != nil { - t.Fatalf("read batch notification artifact: %v", err) - } - var artifact state.BatchDistributorNotificationArtifact - if err := json.Unmarshal(data, &artifact); err != nil { - t.Fatalf("decode batch notification artifact: %v", err) - } - return artifact -} - func loadBatchDataPackage(t *testing.T, path string) promptinput.Package { t.Helper() data, err := os.ReadFile(path) diff --git a/internal/app/prompt_artifact_paths_test.go b/internal/app/prompt_artifact_paths_test.go index b6d2c57..fdbb324 100644 --- a/internal/app/prompt_artifact_paths_test.go +++ b/internal/app/prompt_artifact_paths_test.go @@ -20,12 +20,11 @@ import ( ) const ( - failPromptExecution = "prompt execution" - failMetadata = "metadata" - failGeneratedText = "generated text" - failRenderContext = "render context" - failRenderedReportPath = "rendered report path" - failDistributorNotification = "distributor notification" + failPromptExecution = "prompt execution" + failMetadata = "metadata" + failGeneratedText = "generated text" + failRenderContext = "render context" + failRenderedReportPath = "rendered report path" ) type failingPersistenceStore struct { @@ -67,13 +66,6 @@ func (s *failingPersistenceStore) PrepareRenderedReport(ctx context.Context, res return s.Store.PrepareRenderedReport(ctx, resolved) } -func (s *failingPersistenceStore) SaveDistributorNotification(ctx context.Context, resolved report.Resolved, artifact state.DistributorNotificationArtifact) (string, error) { - if s.failOperation == failDistributorNotification { - return "", errors.New("injected notification persistence failure") - } - return s.Store.SaveDistributorNotification(ctx, resolved, artifact) -} - func (s *failingPersistenceStore) SaveMetadata(ctx context.Context, metadata state.Metadata) (string, error) { s.metadataCalls++ if s.failOperation == failMetadata && s.metadataCalls == s.failMetadataCall { @@ -151,9 +143,8 @@ func TestGeneratePromptReportReturnsOnlyReachedArtifactPaths(t *testing.T) { {name: "execution then metadata", failOperation: failMetadata, failMetadataCall: 2, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true}}, {name: "normalized output then metadata", failOperation: failMetadata, failMetadataCall: 3, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true}}, {name: "render context then metadata", failOperation: failMetadata, failMetadataCall: 4, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true}}, - {name: "managed report then metadata", failOperation: failMetadata, failMetadataCall: 5, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true}}, + {name: "rendered report then metadata", failOperation: failMetadata, failMetadataCall: 5, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true}}, {name: "output then metadata", failOperation: failMetadata, failMetadataCall: 5, output: true, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, output: true}}, - {name: "notification then metadata", failOperation: failMetadata, failMetadataCall: 6, notify: true, want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, notification: true}}, } for _, test := range tests { @@ -250,7 +241,7 @@ func TestCompletedExecutionArtifactTracksDownstreamLifecycle(t *testing.T) { want := state.PromptExecutionPaths{ RawOutputPath: paths.GeneratedTextRaw, GeneratedTextPath: paths.GeneratedText, RenderContextPath: paths.RenderContext, RenderedReportPath: paths.RenderedReport, - OutputPath: paths.output, NotificationPath: paths.Notification, + OutputPath: paths.output, } assertPersistedExecutionPaths(t, req.Store, result.ExecutionPath, want) @@ -314,12 +305,12 @@ func TestCompletedExecutionArtifactRetainsLastPersistedCheckpoint(t *testing.T) wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true}, }, { - name: "managed report write", failOperation: failRenderedReportPath, + name: "rendered report write", failOperation: failRenderedReportPath, wantExecution: reachedExecutionArtifacts{raw: true, normalized: true, renderContext: true}, wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true}, }, { - name: "managed report checkpoint", failOperation: failPromptExecution, failExecutionCall: 4, + name: "rendered report checkpoint", failOperation: failPromptExecution, failExecutionCall: 4, wantExecution: reachedExecutionArtifacts{raw: true, normalized: true, renderContext: true}, wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true}, }, @@ -339,25 +330,10 @@ func TestCompletedExecutionArtifactRetainsLastPersistedCheckpoint(t *testing.T) wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, output: true}, }, { - name: "notification artifact write", failOperation: failDistributorNotification, requestOutput: true, notify: true, + name: "notification operation", requestOutput: true, notify: true, notificationFailure: true, wantExecution: reachedExecutionArtifacts{raw: true, normalized: true, renderContext: true, report: true, output: true}, wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, output: true}, }, - { - name: "notification checkpoint", failOperation: failPromptExecution, failExecutionCall: 6, requestOutput: true, notify: true, - wantExecution: reachedExecutionArtifacts{raw: true, normalized: true, renderContext: true, report: true, output: true}, - wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, output: true, notification: true}, - }, - { - name: "notification metadata", failOperation: failMetadata, failMetadataCall: 6, requestOutput: true, notify: true, - wantExecution: reachedExecutionArtifacts{raw: true, normalized: true, renderContext: true, report: true, output: true, notification: true}, - wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, output: true, notification: true}, - }, - { - name: "notification operation", requestOutput: true, notify: true, notificationFailure: true, - wantExecution: reachedExecutionArtifacts{raw: true, normalized: true, renderContext: true, report: true, output: true, notification: true}, - wantResult: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true, normalized: true, renderContext: true, report: true, output: true, notification: true}, - }, } for _, test := range tests { @@ -409,7 +385,6 @@ type reachedExecutionArtifacts struct { renderContext bool report bool output bool - notification bool } func executionPathsFor(paths promptArtifactPaths, reached reachedExecutionArtifacts) state.PromptExecutionPaths { @@ -429,9 +404,6 @@ func executionPathsFor(paths promptArtifactPaths, reached reachedExecutionArtifa if reached.output { result.OutputPath = paths.output } - if reached.notification { - result.NotificationPath = paths.Notification - } return result } @@ -466,7 +438,6 @@ type reachedPromptArtifacts struct { renderContext bool report bool output bool - notification bool } func promptArtifactRequest(t *testing.T, executor promptexec.Executor) (promptReportRequest, promptArtifactPaths) { @@ -534,7 +505,6 @@ func assertReachedPromptArtifacts(t *testing.T, result *ReportResult, paths prom {"render context", result.RenderContextPath, result.Metadata.RenderContextPath, true, paths.RenderContext, want.renderContext}, {"report", result.ReportPath, result.Metadata.RenderedReportPath, true, paths.RenderedReport, want.report}, {"output", result.OutputPath, "", false, paths.output, want.output}, - {"notification", result.NotificationPath, result.Metadata.NotificationPath, true, paths.Notification, want.notification}, } for _, check := range checks { if check.want && check.got != check.path { diff --git a/internal/app/prompt_generate.go b/internal/app/prompt_generate.go index c1bacfb..3fc2e15 100644 --- a/internal/app/prompt_generate.go +++ b/internal/app/prompt_generate.go @@ -344,10 +344,10 @@ func (w *promptReportWorkflow) finalizeReport(rendered []byte) (*ReportResult, e } finalized, err := finalizeRenderedReport(w.ctx, finalizeRenderedReportRequest{ Config: w.req.Config, Store: w.store, Resolved: w.req.Resolved, Metadata: w.metadata, MetadataPath: w.result.MetadataPath, - ExecutionArtifact: &w.executionArtifact, ManagedReportPath: reportPath, OutputPath: w.req.OutputPath, + ExecutionArtifact: &w.executionArtifact, RenderedReportPath: reportPath, OutputPath: w.req.OutputPath, Notifier: w.req.Notifier, noNotify: w.req.noNotify, }) - w.result.OutputPath, w.result.NotificationPath = finalized.OutputPath, finalized.NotificationPath + w.result.OutputPath = finalized.OutputPath w.result.Metadata, w.result.MetadataPath, w.result.Notification = finalized.Metadata, finalized.MetadataPath, finalized.Notification return w.result, err } diff --git a/internal/app/single_report_workflow_test.go b/internal/app/single_report_workflow_test.go index bdb907d..147b112 100644 --- a/internal/app/single_report_workflow_test.go +++ b/internal/app/single_report_workflow_test.go @@ -197,12 +197,12 @@ func TestGenerateDetailedCompletesRetainedReportWorkflows(t *testing.T) { if !bytes.Equal(executor.request.DataPackage, persisted) { t.Fatal("executor data package differs from exact persisted YAML bytes") } - managed, readErr := os.ReadFile(result.ReportPath) - if readErr != nil || !strings.Contains(string(managed), test.wantOutput) { - t.Fatalf("managed report = %q, error %v, want generated template output %q", managed, readErr, test.wantOutput) + renderedReport, readErr := os.ReadFile(result.ReportPath) + if readErr != nil || !strings.Contains(string(renderedReport), test.wantOutput) { + t.Fatalf("rendered report = %q, error %v, want generated template output %q", renderedReport, readErr, test.wantOutput) } copied, readErr := os.ReadFile(outputPath) - if readErr != nil || !bytes.Equal(copied, managed) || result.OutputPath != outputPath { + if readErr != nil || !bytes.Equal(copied, renderedReport) || result.OutputPath != outputPath { t.Fatalf("output mismatch/error/path = %v/%q", readErr, result.OutputPath) } if len(notifier.requests) != 1 || notifier.requests[0].ReportPath != outputPath { @@ -217,9 +217,9 @@ func TestGenerateDetailedCompletesRetainedReportWorkflows(t *testing.T) { if strings.Join(notifier.requests[0].BundlePaths, "\n") != strings.Join(wantBundlePaths, "\n") { t.Fatalf("bundle paths = %#v, want %#v", notifier.requests[0].BundlePaths, wantBundlePaths) } - managedName := filepath.Base(result.ReportPath) - if !strings.HasPrefix(managedName, "report.") || !strings.Contains(managedName, "_"+test.name) || !strings.HasSuffix(managedName, ".md") || filepath.Base(result.OutputPath) != test.name+".md" { - t.Fatalf("output names = managed %q copy %q", result.ReportPath, result.OutputPath) + renderedName := filepath.Base(result.ReportPath) + if !strings.HasPrefix(renderedName, "report.") || !strings.Contains(renderedName, "_"+test.name) || !strings.HasSuffix(renderedName, ".md") || filepath.Base(result.OutputPath) != test.name+".md" { + t.Fatalf("output names = rendered %q copy %q", result.ReportPath, result.OutputPath) } }) } @@ -465,7 +465,6 @@ func TestGenerateDetailedRetainsInspectableArtifactsAfterApplicationFailures(t * wantContext bool wantReport bool wantOutput bool - wantNotify bool }{ {name: "generated text decode", raw: `{`, wantRaw: true}, {name: "generated text domain", raw: `{}`, wantRaw: true}, @@ -484,7 +483,7 @@ func TestGenerateDetailedRetainsInspectableArtifactsAfterApplicationFailures(t * }, wantRaw: true, wantNormalized: true, wantContext: true}, {name: "notification", raw: validDailyWorkflowJSON(), configure: func(_ *GenerateRequest, notifier *workflowNotifier) { notifier.err = errors.New("notification rejected") - }, wantRaw: true, wantNormalized: true, wantContext: true, wantReport: true, wantOutput: true, wantNotify: true}, + }, wantRaw: true, wantNormalized: true, wantContext: true, wantReport: true, wantOutput: true}, } for _, test := range tests { @@ -512,8 +511,8 @@ func TestGenerateDetailedRetainsInspectableArtifactsAfterApplicationFailures(t * } if (result.GeneratedTextRawPath != "") != test.wantRaw || (result.GeneratedTextPath != "") != test.wantNormalized || (result.RenderContextPath != "") != test.wantContext || (result.ReportPath != "") != test.wantReport || - (result.OutputPath != "") != test.wantOutput || (result.NotificationPath != "") != test.wantNotify { - t.Fatalf("reached paths = raw %q normalized %q context %q report %q output %q notification %q", result.GeneratedTextRawPath, result.GeneratedTextPath, result.RenderContextPath, result.ReportPath, result.OutputPath, result.NotificationPath) + (result.OutputPath != "") != test.wantOutput { + t.Fatalf("reached paths = raw %q normalized %q context %q report %q output %q", result.GeneratedTextRawPath, result.GeneratedTextPath, result.RenderContextPath, result.ReportPath, result.OutputPath) } if test.wantRaw { persisted, readErr := os.ReadFile(result.GeneratedTextRawPath) @@ -524,6 +523,9 @@ func TestGenerateDetailedRetainsInspectableArtifactsAfterApplicationFailures(t * if test.name == "notification" && (len(notifier.requests) != 1 || notifier.requests[0].ReportPath != result.OutputPath) { t.Fatalf("notification requests = %#v", notifier.requests) } + if test.name == "notification" && result.Metadata.NotificationPath != "" { + t.Fatalf("notification metadata retains a receipt path: %#v", result.Metadata) + } }) } } diff --git a/internal/cli/output.go b/internal/cli/output.go index 37cbe44..4e6426c 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -63,9 +63,6 @@ func writeBatchStatus(stderr io.Writer, result *app.BatchResult) { if result.Notification.BundleID != "" { _, _ = fmt.Fprintf(stderr, " bundleId=%q", result.Notification.BundleID) } - if result.Notification.Path != "" { - _, _ = fmt.Fprintf(stderr, " path=%q", result.Notification.Path) - } if result.Notification.Error != "" { _, _ = fmt.Fprintf(stderr, " error=%q", result.Notification.Error) } diff --git a/internal/cli/result.go b/internal/cli/result.go index e119e0e..f7a9904 100644 --- a/internal/cli/result.go +++ b/internal/cli/result.go @@ -35,7 +35,6 @@ type generateSummary struct { GeneratedTextRawPath string `json:"generatedTextRawPath,omitempty"` GeneratedTextPath string `json:"generatedTextPath,omitempty"` RenderContextPath string `json:"renderContextPath,omitempty"` - NotificationPath string `json:"notificationPath,omitempty"` Notification *generateNotificationSummary `json:"notification,omitempty"` Error string `json:"error,omitempty"` } @@ -92,7 +91,6 @@ func newGenerateSummary(result *app.ReportResult, err error) generateSummary { summary.GeneratedTextRawPath = result.GeneratedTextRawPath summary.GeneratedTextPath = result.GeneratedTextPath summary.RenderContextPath = result.RenderContextPath - summary.NotificationPath = result.NotificationPath summary.Notification = newGenerateNotificationSummary(result.Notification) if err != nil { summary.Status = summaryStatusFailed diff --git a/internal/cli/result_test.go b/internal/cli/result_test.go index 0e7d2a6..d64b4d6 100644 --- a/internal/cli/result_test.go +++ b/internal/cli/result_test.go @@ -29,7 +29,6 @@ func TestNewGenerateSummaryForGeneratedTextReport(t *testing.T) { GeneratedTextRawPath: "/runs/hourly/generated_text_raw.json", GeneratedTextPath: "/runs/hourly/generated_text.json", RenderContextPath: "/runs/hourly/render_context.json", - NotificationPath: "/runs/hourly/notification.json", Metadata: state.Metadata{ ReportID: report.Hourly, PromptID: "weather.hourly_generated_text", @@ -99,8 +98,8 @@ func TestNewGenerateSummaryOmitsNotificationWhenNotAttempted(t *testing.T) { if summary.ReportID != report.Daily || summary.ReportName != "Daily Report" || summary.Status != "succeeded" { t.Fatalf("summary = %#v, want successful daily summary", summary) } - if summary.Notification != nil || summary.NotificationPath != "" { - t.Fatalf("notification summary/path = %#v/%q, want omitted", summary.Notification, summary.NotificationPath) + if summary.Notification != nil { + t.Fatalf("notification summary = %#v, want omitted", summary.Notification) } data, err := json.Marshal(summary) if err != nil { @@ -141,12 +140,11 @@ func TestNewGenerateSummaryOmitsUnreachedArtifactPaths(t *testing.T) { func TestNewGenerateSummaryForNotificationFailure(t *testing.T) { generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC) result := &app.ReportResult{ - DataPackagePath: "/runs/hourly/data_package.yaml", - PreparationPath: "/runs/hourly/preparation.json", - ReportPath: "/runs/hourly/report.md", - OutputPath: "/copies/hourly.md", - MetadataPath: "/runs/hourly/metadata.json", - NotificationPath: "/runs/hourly/notification.json", + DataPackagePath: "/runs/hourly/data_package.yaml", + PreparationPath: "/runs/hourly/preparation.json", + ReportPath: "/runs/hourly/report.md", + OutputPath: "/copies/hourly.md", + MetadataPath: "/runs/hourly/metadata.json", Metadata: state.Metadata{ ReportID: report.Hourly, PromptID: "weather.hourly_generated_text", @@ -162,8 +160,15 @@ func TestNewGenerateSummaryForNotificationFailure(t *testing.T) { if summary.Status != "failed" || summary.Error != err.Error() { t.Fatalf("status/error = %q/%q, want failed notification error", summary.Status, summary.Error) } - if summary.NotificationPath != "/runs/hourly/notification.json" || summary.ReportPath == "" || summary.MetadataPath == "" { - t.Fatalf("artifact paths = report %q metadata %q notification %q, want inspectable paths", summary.ReportPath, summary.MetadataPath, summary.NotificationPath) + if summary.ReportPath == "" || summary.MetadataPath == "" { + t.Fatalf("artifact paths = report %q metadata %q, want retained output provenance", summary.ReportPath, summary.MetadataPath) + } + data, marshalErr := json.Marshal(summary) + if marshalErr != nil { + t.Fatalf("Marshal() error = %v", marshalErr) + } + if strings.Contains(string(data), "notificationPath") { + t.Fatalf("summary JSON includes a notification receipt path:\n%s", data) } }