Correct artifact path bookkeeping

This commit is contained in:
2026-07-31 16:37:00 +00:00
parent b96f40e5ca
commit 25782447eb
8 changed files with 792 additions and 845 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -343,13 +343,6 @@ 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(planned) item := batchReportResult(planned)
if paths, err := store.Paths(resolved); err == nil {
item.DataPackagePath = paths.DataPackage
item.PreparationPath = paths.Preparation
item.ExecutionPath = paths.Execution
item.ReportPath = paths.RenderedReport
item.MetadataPath = paths.Metadata
}
outputPath := plannedBatchOutputPath(req.OutputDir, planned) outputPath := plannedBatchOutputPath(req.OutputDir, planned)
reportResult, err := generatePromptReport(ctx, promptReportRequest{ reportResult, err := generatePromptReport(ctx, promptReportRequest{
GenerateRequest: GenerateRequest{ GenerateRequest: GenerateRequest{
@@ -540,6 +533,7 @@ type finalizeRenderedReportRequest struct {
Store state.Store Store state.Store
Resolved report.Resolved Resolved report.Resolved
Metadata state.Metadata Metadata state.Metadata
MetadataPath string
ManagedReportPath string ManagedReportPath string
OutputPath string OutputPath string
Notifier Notifier Notifier Notifier
@@ -563,27 +557,24 @@ func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportReque
return finalizeRenderedReportResult{}, fmt.Errorf("managed report path is required for report %q", req.Resolved.Definition.ID) return finalizeRenderedReportResult{}, fmt.Errorf("managed report path is required for report %q", req.Resolved.Definition.ID)
} }
metadata := req.Metadata result := finalizeRenderedReportResult{Metadata: req.Metadata, MetadataPath: req.MetadataPath}
metadata.RenderedReportPath = req.ManagedReportPath
outputPath := req.ManagedReportPath
if req.OutputPath != "" { if req.OutputPath != "" {
outputPath = req.OutputPath
if req.GenerationErr == nil && req.OutputPath != req.ManagedReportPath { if req.GenerationErr == nil && req.OutputPath != req.ManagedReportPath {
if err := fileutil.CopyFileAtomic(req.ManagedReportPath, req.OutputPath); err != nil { if err := fileutil.CopyFileAtomic(req.ManagedReportPath, req.OutputPath); err != nil {
return finalizeRenderedReportResult{}, err return result, err
} }
} }
result.OutputPath = req.OutputPath
} }
metadata := req.Metadata
metadata.RenderedReportPath = req.ManagedReportPath
metadataPath, err := req.Store.SaveMetadata(ctx, metadata) metadataPath, err := req.Store.SaveMetadata(ctx, metadata)
if err != nil { if err != nil {
return finalizeRenderedReportResult{}, err return result, err
}
result := finalizeRenderedReportResult{
OutputPath: outputPath,
Metadata: metadata,
MetadataPath: metadataPath,
} }
result.Metadata = metadata
result.MetadataPath = metadataPath
if req.GenerationErr != nil { if req.GenerationErr != nil {
return result, req.GenerationErr return result, req.GenerationErr
} }
@@ -593,14 +584,16 @@ func finalizeRenderedReport(ctx context.Context, req finalizeRenderedReportReque
notification, notificationPath, err := notifyReport(ctx, req.Config, req.Resolved, req.ManagedReportPath, metadata, req.Notifier, req.Store) notification, notificationPath, err := notifyReport(ctx, req.Config, req.Resolved, req.ManagedReportPath, metadata, req.Notifier, req.Store)
if notificationPath != "" { if notificationPath != "" {
result.NotificationPath = notificationPath
result.Notification = notification
metadata.NotificationPath = notificationPath metadata.NotificationPath = notificationPath
result.Metadata = metadata
metadataPath, saveErr := req.Store.SaveMetadata(ctx, metadata) metadataPath, saveErr := req.Store.SaveMetadata(ctx, metadata)
if saveErr != nil { if saveErr != nil {
return finalizeRenderedReportResult{}, saveErr return result, saveErr
} }
result.Metadata = metadata result.Metadata = metadata
result.MetadataPath = metadataPath result.MetadataPath = metadataPath
result.NotificationPath = notificationPath
} }
result.Notification = notification result.Notification = notification
if err != nil { if err != nil {

View File

@@ -2,7 +2,9 @@ package app
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect" "gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
@@ -39,6 +41,28 @@ func TestRunBatchDetailedInspectsEveryCandidateBeforeCollection(t *testing.T) {
} }
} }
func TestCopyBatchReportPathsLeavesUnreachedPathsEmpty(t *testing.T) {
item := BatchReportResult{}
copyBatchReportPaths(&item, &ReportResult{
DataPackagePath: "/runs/daily/data_package.yaml",
PreparationPath: "/runs/daily/preparation.json",
})
data, err := json.Marshal(item)
if err != nil {
t.Fatalf("Marshal() error = %v", err)
}
text := string(data)
for _, omitted := range []string{"executionPath", "reportPath", "outputPath", "metadataPath", "notificationPath"} {
if strings.Contains(text, omitted) {
t.Fatalf("batch item includes unreached field %q:\n%s", omitted, text)
}
}
if !strings.Contains(text, "dataPackagePath") || !strings.Contains(text, "preparationPath") {
t.Fatalf("batch item omits reached paths:\n%s", text)
}
}
type collectorFunc func(context.Context, collect.Request) (*collect.Result, error) type collectorFunc func(context.Context, collect.Request) (*collect.Result, error)
func (f collectorFunc) Run(ctx context.Context, req collect.Request) (*collect.Result, error) { func (f collectorFunc) Run(ctx context.Context, req collect.Request) (*collect.Result, error) {

View File

@@ -0,0 +1,272 @@
package app
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
)
const (
failPromptExecution = "prompt execution"
failMetadata = "metadata"
)
type failingPersistenceStore struct {
state.Store
failOperation string
failMetadataCall int
metadataCalls int
}
func (s *failingPersistenceStore) SavePromptExecution(ctx context.Context, resolved report.Resolved, artifact state.PromptExecutionArtifact) (string, error) {
if s.failOperation == failPromptExecution {
return "", errors.New("injected prompt execution persistence failure")
}
return s.Store.SavePromptExecution(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 {
return "", errors.New("injected metadata persistence failure")
}
return s.Store.SaveMetadata(ctx, metadata)
}
type artifactPathExecutor struct {
beforePreparationErr error
afterPreparationErr error
validation promptexec.ValidationStatus
}
func (e artifactPathExecutor) InspectPrompt(context.Context, string, string) (promptexec.PromptInspection, error) {
return promptexec.PromptInspection{}, errors.New("unexpected inspection")
}
func (e artifactPathExecutor) InspectProfile(context.Context, string) (promptexec.ProfileInspection, error) {
return promptexec.ProfileInspection{}, errors.New("unexpected inspection")
}
func (e artifactPathExecutor) Execute(_ context.Context, req promptexec.ExecuteRequest, callback promptexec.PreparationCallback) (*promptexec.Execution, error) {
if e.beforePreparationErr != nil {
return nil, e.beforePreparationErr
}
now := time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
if err := callback(promptexec.Preparation{
PromptID: req.PromptID, PromptVersion: req.PromptVersion, PromptHash: "prompt-hash",
RenderedPromptHash: "rendered-hash", ProfileID: req.ProfileID, BackendID: "test",
ModelName: "test-model", DataPackagePath: req.DataPackagePath, StartedAt: now, EndedAt: now,
}, nil); err != nil {
return nil, err
}
if e.afterPreparationErr != nil {
return nil, e.afterPreparationErr
}
validation := e.validation
if validation == "" {
validation = promptexec.ValidationPassed
}
return &promptexec.Execution{
RunID: "provider-run", PromptID: req.PromptID, PromptVersion: req.PromptVersion,
PromptHash: "prompt-hash", RenderedPromptHash: "rendered-hash", ProfileID: req.ProfileID,
BackendID: "test", ModelName: "test-model", GeneratedHash: "generated-hash",
StartedAt: now, EndedAt: now, DataPackagePath: req.DataPackagePath,
RawOutput: []byte(`{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}`),
Validation: promptexec.NewValidation(validation, "json_schema", "daily.generated_text.schema.json", nil),
}, nil
}
type successfulNotifier struct{}
func (successfulNotifier) Notify(context.Context, NotificationRequest) (*NotificationResult, error) {
return &NotificationResult{RunID: "notification-run", Status: "succeeded", UploadStatus: "accepted"}, nil
}
func TestGeneratePromptReportReturnsOnlyReachedArtifactPaths(t *testing.T) {
tests := []struct {
name string
failOperation string
failMetadataCall int
outputCopy bool
notify bool
want reachedPromptArtifacts
}{
{name: "preparation then metadata", failOperation: failMetadata, failMetadataCall: 1, want: reachedPromptArtifacts{preparation: true}},
{name: "raw output then execution", failOperation: failPromptExecution, want: reachedPromptArtifacts{preparation: true, metadata: true, raw: true}},
{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: "output copy then metadata", failOperation: failMetadata, failMetadataCall: 5, outputCopy: 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 {
t.Run(test.name, func(t *testing.T) {
req, paths := promptArtifactRequest(t, artifactPathExecutor{})
store := &failingPersistenceStore{Store: req.Store, failOperation: test.failOperation, failMetadataCall: test.failMetadataCall}
req.Store = store
if test.outputCopy {
req.OutputPath = filepath.Join(t.TempDir(), "daily.md")
paths.output = req.OutputPath
}
if test.notify {
req.Config.Notify.Distributor.Enabled = true
req.Notifier = successfulNotifier{}
req.noNotify = false
}
result, err := generatePromptReport(context.Background(), req)
if err == nil || result == nil {
t.Fatalf("generatePromptReport() result/error = %#v/%v, want partial result and failure", result, err)
}
assertReachedPromptArtifacts(t, result, paths, test.want)
})
}
}
func TestGeneratePromptReportFailureReceiptsExposeReachedPaths(t *testing.T) {
tests := []struct {
name string
executor artifactPathExecutor
want reachedPromptArtifacts
}{
{
name: "preparation failure",
executor: artifactPathExecutor{beforePreparationErr: promptexec.NewError(promptexec.Generation, "prepare failed", nil)},
want: reachedPromptArtifacts{preparation: true, metadata: true},
},
{
name: "operational execution failure",
executor: artifactPathExecutor{afterPreparationErr: promptexec.NewError(promptexec.Generation, "provider failed", nil)},
want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true},
},
{
name: "completed validation rejection",
executor: artifactPathExecutor{validation: promptexec.ValidationFailed},
want: reachedPromptArtifacts{preparation: true, execution: true, metadata: true, raw: true},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req, paths := promptArtifactRequest(t, test.executor)
result, err := generatePromptReport(context.Background(), req)
if err == nil || result == nil {
t.Fatalf("generatePromptReport() result/error = %#v/%v, want partial result and failure", result, err)
}
assertReachedPromptArtifacts(t, result, paths, test.want)
})
}
}
type promptArtifactPaths struct {
state.ArtifactPaths
output string
}
type reachedPromptArtifacts struct {
preparation bool
execution bool
metadata bool
raw bool
normalized bool
renderContext bool
report bool
output bool
notification bool
}
func promptArtifactRequest(t *testing.T, executor promptexec.Executor) (promptReportRequest, promptArtifactPaths) {
t.Helper()
cfg := config.Defaults()
cfg.Workspace.Root = t.TempDir()
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg, Report: ReportDaily, Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
}
bundleData, err := os.ReadFile(filepath.Join("..", "forecast", "testdata", "daily_bundle.json"))
if err != nil {
t.Fatalf("read daily fixture: %v", err)
}
var bundle weatherdata.Bundle
if err := json.Unmarshal(bundleData, &bundle); err != nil {
t.Fatalf("decode daily fixture: %v", err)
}
filesystemStore, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
t.Fatalf("NewFilesystemStore() error = %v", err)
}
paths, err := filesystemStore.Paths(resolved)
if err != nil {
t.Fatalf("Paths() error = %v", err)
}
debugWriter, err := state.NewPromptDebugWriter("")
if err != nil {
t.Fatalf("NewPromptDebugWriter() error = %v", err)
}
return promptReportRequest{
GenerateRequest: GenerateRequest{Config: cfg, Report: ReportDaily, Executor: executor, Store: filesystemStore},
Resolved: resolved, Collection: collect.Result{Bundle: &bundle},
Inspection: PromptInspectionResult{
PromptID: resolved.Definition.PromptID, PromptVersion: resolved.Definition.PromptVersion,
PromptHash: "prompt-hash", ProfileID: "test-profile", BackendID: "test", ModelName: "test-model",
},
DebugWriter: debugWriter, noNotify: true,
}, promptArtifactPaths{ArtifactPaths: paths}
}
func assertReachedPromptArtifacts(t *testing.T, result *ReportResult, paths promptArtifactPaths, want reachedPromptArtifacts) {
t.Helper()
if result.ModuleSnapshotPath != paths.ModuleSnapshot || result.DataPackagePath != paths.DataPackage {
t.Fatalf("base paths = module %q data %q, want %q and %q", result.ModuleSnapshotPath, result.DataPackagePath, paths.ModuleSnapshot, paths.DataPackage)
}
if result.Metadata.ModuleSnapshotPath != paths.ModuleSnapshot || result.Metadata.DataPackagePath != paths.DataPackage || result.Metadata.MetadataPath != paths.Metadata {
t.Fatalf("metadata base paths = %#v, want reached module/data paths and metadata destination", result.Metadata)
}
checks := []struct {
name string
got string
metadataGot string
inMetadata bool
path string
want bool
}{
{"preparation", result.PreparationPath, result.Metadata.PreparationPath, true, paths.Preparation, want.preparation},
{"execution", result.ExecutionPath, result.Metadata.ExecutionPath, true, paths.Execution, want.execution},
{"metadata", result.MetadataPath, "", false, paths.Metadata, want.metadata},
{"raw", result.GeneratedTextRawPath, result.Metadata.GeneratedTextRawPath, true, paths.GeneratedTextRaw, want.raw},
{"normalized", result.GeneratedTextPath, result.Metadata.GeneratedTextPath, true, paths.GeneratedText, want.normalized},
{"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 {
t.Errorf("%s path = %q, want reached path %q", check.name, check.got, check.path)
}
if check.want && check.inMetadata && check.metadataGot != check.path {
t.Errorf("metadata %s path = %q, want reached path %q", check.name, check.metadataGot, check.path)
}
if !check.want && check.got != "" {
t.Errorf("%s path = %q, want empty because artifact was not reached", check.name, check.got)
}
if !check.want && check.inMetadata && check.metadataGot != "" {
t.Errorf("metadata %s path = %q, want empty because artifact was not reached", check.name, check.metadataGot)
}
}
}

View File

@@ -41,7 +41,7 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := &ReportResult{ReportPath: paths.RenderedReport} result := &ReportResult{}
priorSnapshot, err := store.FindPriorSnapshot(ctx, req.Resolved) priorSnapshot, err := store.FindPriorSnapshot(ctx, req.Resolved)
if err != nil { if err != nil {
@@ -72,13 +72,6 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
metadata := state.BuildPromptMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{ metadata := state.BuildPromptMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{
ModuleSnapshot: moduleSnapshotPath, ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage,
Preparation: paths.Preparation,
Execution: paths.Execution,
RenderedReport: paths.RenderedReport,
GeneratedTextRaw: paths.GeneratedTextRaw,
GeneratedText: paths.GeneratedText,
RenderContext: paths.RenderContext,
}) })
result.Metadata = metadata result.Metadata = metadata
dataPackage, err := promptinput.Build(promptinput.BuildRequest{ dataPackage, err := promptinput.Build(promptinput.BuildRequest{
@@ -179,11 +172,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, saveErr return result, saveErr
} }
metadata.PreparationPath = path metadata.PreparationPath = path
result.PreparationPath = path
result.Metadata = metadata
metadataPath, saveErr := store.SaveMetadata(ctx, metadata) metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
if saveErr != nil { if saveErr != nil {
return result, saveErr return result, saveErr
} }
result.PreparationPath, result.Metadata, result.MetadataPath = path, metadata, metadataPath result.MetadataPath = metadataPath
return result, generatedReportError(req.Resolved, metadata.RunID, "prepare prompt", executeErr) return result, generatedReportError(req.Resolved, metadata.RunID, "prepare prompt", executeErr)
} }
if promptexec.CategoryOf(executeErr) != "" { if promptexec.CategoryOf(executeErr) != "" {
@@ -193,11 +188,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, saveErr return result, saveErr
} }
metadata.ExecutionPath = path metadata.ExecutionPath = path
result.ExecutionPath = path
result.Metadata = metadata
metadataPath, saveErr := store.SaveMetadata(ctx, metadata) metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
if saveErr != nil { if saveErr != nil {
return result, saveErr return result, saveErr
} }
result.ExecutionPath, result.Metadata, result.MetadataPath = path, metadata, metadataPath result.MetadataPath = metadataPath
} }
return result, generatedReportError(req.Resolved, metadata.RunID, "execute prompt", executeErr) return result, generatedReportError(req.Resolved, metadata.RunID, "execute prompt", executeErr)
} }
@@ -209,11 +206,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, saveErr return result, saveErr
} }
metadata.ExecutionPath = executionPath metadata.ExecutionPath = executionPath
result.ExecutionPath = executionPath
result.Metadata = metadata
metadataPath, saveErr := store.SaveMetadata(ctx, metadata) metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
if saveErr != nil { if saveErr != nil {
return result, saveErr return result, saveErr
} }
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, metadataPath result.MetadataPath = metadataPath
return result, generatedReportError(req.Resolved, metadata.RunID, "execute prompt", err) return result, generatedReportError(req.Resolved, metadata.RunID, "execute prompt", err)
} }
debugPath, err := req.DebugWriter.WriteExecution(debugRef, *execution) debugPath, err := req.DebugWriter.WriteExecution(debugRef, *execution)
@@ -232,11 +231,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, saveErr return result, saveErr
} }
metadata.ExecutionPath = executionPath metadata.ExecutionPath = executionPath
result.ExecutionPath = executionPath
result.Metadata = metadata
metadataPath, saveErr := store.SaveMetadata(ctx, metadata) metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
if saveErr != nil { if saveErr != nil {
return result, saveErr return result, saveErr
} }
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, metadataPath result.MetadataPath = metadataPath
return result, generatedReportError(req.Resolved, metadata.RunID, "validate prompt execution", err) return result, generatedReportError(req.Resolved, metadata.RunID, "validate prompt execution", err)
} }
rawPath, err := store.SaveGeneratedTextRaw(ctx, req.Resolved, execution.RawOutput) rawPath, err := store.SaveGeneratedTextRaw(ctx, req.Resolved, execution.RawOutput)
@@ -244,6 +245,8 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, err return result, err
} }
result.GeneratedTextRawPath = rawPath result.GeneratedTextRawPath = rawPath
metadata.GeneratedTextRawPath = rawPath
result.Metadata = metadata
executionArtifact := state.PromptExecutionArtifact{ executionArtifact := state.PromptExecutionArtifact{
SchemaVersion: state.PromptExecutionSchemaVersion, SchemaVersion: state.PromptExecutionSchemaVersion,
ReportID: req.Resolved.Definition.ID, RunID: metadata.RunID, ReportID: req.Resolved.Definition.ID, RunID: metadata.RunID,
@@ -262,12 +265,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, err return result, err
} }
metadata.ExecutionPath = executionPath metadata.ExecutionPath = executionPath
metadata.GeneratedTextRawPath = rawPath result.ExecutionPath = executionPath
result.Metadata = metadata
metadataPath, err := store.SaveMetadata(ctx, metadata) metadataPath, err := store.SaveMetadata(ctx, metadata)
if err != nil { if err != nil {
return result, err return result, err
} }
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, metadataPath result.MetadataPath = metadataPath
if execution.Validation.Status == promptexec.ValidationFailed { if execution.Validation.Status == promptexec.ValidationFailed {
return result, generatedReportError(req.Resolved, metadata.RunID, "validate prompt execution", promptexec.NewError(promptexec.ValidationRejected, "prompt output did not satisfy its schema", nil)) return result, generatedReportError(req.Resolved, metadata.RunID, "validate prompt execution", promptexec.NewError(promptexec.ValidationRejected, "prompt output did not satisfy its schema", nil))
} }
@@ -281,11 +285,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, err return result, err
} }
metadata.GeneratedTextPath = generatedTextPath metadata.GeneratedTextPath = generatedTextPath
result.GeneratedTextPath = generatedTextPath
result.Metadata = metadata
metadataPath, err = store.SaveMetadata(ctx, metadata) metadataPath, err = store.SaveMetadata(ctx, metadata)
if err != nil { if err != nil {
return result, err return result, err
} }
result.GeneratedTextPath, result.Metadata, result.MetadataPath = generatedTextPath, metadata, metadataPath result.MetadataPath = metadataPath
renderContext, err := handler.BuildRenderContext(briefingMetadata, moduleSnapshot, reportFacts.Collected, reportFacts.Derived, generatedText) renderContext, err := handler.BuildRenderContext(briefingMetadata, moduleSnapshot, reportFacts.Collected, reportFacts.Derived, generatedText)
if err != nil { if err != nil {
@@ -296,11 +302,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, err return result, err
} }
metadata.RenderContextPath = renderContextPath metadata.RenderContextPath = renderContextPath
result.RenderContextPath = renderContextPath
result.Metadata = metadata
metadataPath, err = store.SaveMetadata(ctx, metadata) metadataPath, err = store.SaveMetadata(ctx, metadata)
if err != nil { if err != nil {
return result, err return result, err
} }
result.RenderContextPath, result.Metadata, result.MetadataPath = renderContextPath, metadata, metadataPath result.MetadataPath = metadataPath
rendered, err := handler.Render(renderContext) rendered, err := handler.Render(renderContext)
if err != nil { if err != nil {
@@ -314,8 +322,10 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
return result, err return result, err
} }
result.ReportPath = reportPath result.ReportPath = reportPath
metadata.RenderedReportPath = reportPath
result.Metadata = metadata
finalized, err := finalizeRenderedReport(ctx, finalizeRenderedReportRequest{ finalized, err := finalizeRenderedReport(ctx, finalizeRenderedReportRequest{
Config: req.Config, Store: store, Resolved: req.Resolved, Metadata: metadata, Config: req.Config, Store: store, Resolved: req.Resolved, Metadata: metadata, MetadataPath: result.MetadataPath,
ManagedReportPath: reportPath, OutputPath: req.OutputPath, Notifier: req.Notifier, noNotify: req.noNotify, ManagedReportPath: reportPath, OutputPath: req.OutputPath, Notifier: req.Notifier, noNotify: req.noNotify,
}) })
result.OutputPath, result.NotificationPath = finalized.OutputPath, finalized.NotificationPath result.OutputPath, result.NotificationPath = finalized.OutputPath, finalized.NotificationPath

View File

@@ -113,6 +113,31 @@ func TestNewGenerateSummaryOmitsNotificationWhenNotAttempted(t *testing.T) {
} }
} }
func TestNewGenerateSummaryOmitsUnreachedArtifactPaths(t *testing.T) {
result := &app.ReportResult{
DataPackagePath: "/runs/daily/data_package.yaml",
PreparationPath: "/runs/daily/preparation.json",
Metadata: state.Metadata{
ReportID: report.Daily,
RunID: "20260529T133000Z_daily",
},
}
data, err := json.Marshal(newGenerateSummary(result, errors.New("metadata write failed")))
if err != nil {
t.Fatalf("Marshal() error = %v", err)
}
text := string(data)
for _, omitted := range []string{"executionPath", "reportPath", "outputPath", "metadataPath", "generatedTextRawPath", "generatedTextPath", "renderContextPath", "notificationPath"} {
if strings.Contains(text, omitted) {
t.Fatalf("partial summary includes unreached field %q:\n%s", omitted, text)
}
}
if !strings.Contains(text, "dataPackagePath") || !strings.Contains(text, "preparationPath") {
t.Fatalf("partial summary omits reached paths:\n%s", text)
}
}
func TestNewGenerateSummaryForNotificationFailure(t *testing.T) { func TestNewGenerateSummaryForNotificationFailure(t *testing.T) {
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC) generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
result := &app.ReportResult{ result := &app.ReportResult{

View File

@@ -182,9 +182,8 @@ func BuildPromptMetadataFromBriefingMetadata(resolved report.Resolved, briefingM
Location: copyLocation(briefingMetadata.Location), SourceLocationID: briefingMetadata.SourceLocationID, Location: copyLocation(briefingMetadata.Location), SourceLocationID: briefingMetadata.SourceLocationID,
SourceLocation: briefingMetadata.SourceLocation, Sources: briefingMetadata.Sources, SourceLocation: briefingMetadata.SourceLocation, Sources: briefingMetadata.Sources,
SourceWarnings: briefingMetadata.SourceWarnings, ModuleSnapshotPath: paths.ModuleSnapshot, SourceWarnings: briefingMetadata.SourceWarnings, ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage, RenderedReportPath: paths.RenderedReport, DataPackagePath: paths.DataPackage,
GeneratedTextSchemaID: resolved.Definition.GeneratedTextSchemaID, GeneratedTextRawPath: paths.GeneratedTextRaw, GeneratedTextSchemaID: resolved.Definition.GeneratedTextSchemaID,
GeneratedTextPath: paths.GeneratedText, RenderContextPath: paths.RenderContext,
} }
} }

View File

@@ -0,0 +1,38 @@
package state
import (
"testing"
"time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
)
func TestBuildPromptMetadataIncludesOnlyExistingArtifacts(t *testing.T) {
resolved, err := report.DefaultRegistry().Resolve(report.Daily, report.ResolveRequest{
Now: time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC),
Date: time.Date(2026, 5, 29, 0, 0, 0, 0, time.UTC), Location: time.UTC,
})
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
metadata := BuildPromptMetadataFromBriefingMetadata(resolved, briefing.Metadata{}, ArtifactPaths{
ModuleSnapshot: "/saved/modules.json",
Metadata: "/destination/metadata.json",
DataPackage: "/saved/data.yaml",
Preparation: "/future/preparation.json",
Execution: "/future/execution.json",
Notification: "/future/notification.json",
RenderedReport: "/future/report.md",
GeneratedTextRaw: "/future/raw.json",
GeneratedText: "/future/generated.json",
RenderContext: "/future/context.json",
})
if metadata.ModuleSnapshotPath != "/saved/modules.json" || metadata.DataPackagePath != "/saved/data.yaml" || metadata.MetadataPath != "/destination/metadata.json" {
t.Fatalf("existing paths = %#v, want module, data package, and metadata destination", metadata)
}
if metadata.PreparationPath != "" || metadata.ExecutionPath != "" || metadata.NotificationPath != "" || metadata.RenderedReportPath != "" || metadata.GeneratedTextRawPath != "" || metadata.GeneratedTextPath != "" || metadata.RenderContextPath != "" {
t.Fatalf("metadata includes paths for unreached artifacts: %#v", metadata)
}
}