Correct artifact path bookkeeping
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -343,13 +343,6 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
|
||||
for _, planned := range plannedReports {
|
||||
resolved := planned.Resolved
|
||||
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)
|
||||
reportResult, err := generatePromptReport(ctx, promptReportRequest{
|
||||
GenerateRequest: GenerateRequest{
|
||||
@@ -540,6 +533,7 @@ type finalizeRenderedReportRequest struct {
|
||||
Store state.Store
|
||||
Resolved report.Resolved
|
||||
Metadata state.Metadata
|
||||
MetadataPath string
|
||||
ManagedReportPath string
|
||||
OutputPath string
|
||||
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)
|
||||
}
|
||||
|
||||
metadata := req.Metadata
|
||||
metadata.RenderedReportPath = req.ManagedReportPath
|
||||
outputPath := req.ManagedReportPath
|
||||
result := finalizeRenderedReportResult{Metadata: req.Metadata, MetadataPath: req.MetadataPath}
|
||||
if req.OutputPath != "" {
|
||||
outputPath = req.OutputPath
|
||||
if req.GenerationErr == nil && req.OutputPath != req.ManagedReportPath {
|
||||
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)
|
||||
if err != nil {
|
||||
return finalizeRenderedReportResult{}, err
|
||||
}
|
||||
result := finalizeRenderedReportResult{
|
||||
OutputPath: outputPath,
|
||||
Metadata: metadata,
|
||||
MetadataPath: metadataPath,
|
||||
return result, err
|
||||
}
|
||||
result.Metadata = metadata
|
||||
result.MetadataPath = metadataPath
|
||||
if req.GenerationErr != nil {
|
||||
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)
|
||||
if notificationPath != "" {
|
||||
result.NotificationPath = notificationPath
|
||||
result.Notification = notification
|
||||
metadata.NotificationPath = notificationPath
|
||||
result.Metadata = metadata
|
||||
metadataPath, saveErr := req.Store.SaveMetadata(ctx, metadata)
|
||||
if saveErr != nil {
|
||||
return finalizeRenderedReportResult{}, saveErr
|
||||
return result, saveErr
|
||||
}
|
||||
result.Metadata = metadata
|
||||
result.MetadataPath = metadataPath
|
||||
result.NotificationPath = notificationPath
|
||||
}
|
||||
result.Notification = notification
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,9 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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)
|
||||
|
||||
func (f collectorFunc) Run(ctx context.Context, req collect.Request) (*collect.Result, error) {
|
||||
|
||||
272
internal/app/prompt_artifact_paths_test.go
Normal file
272
internal/app/prompt_artifact_paths_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ReportResult{ReportPath: paths.RenderedReport}
|
||||
result := &ReportResult{}
|
||||
|
||||
priorSnapshot, err := store.FindPriorSnapshot(ctx, req.Resolved)
|
||||
if err != nil {
|
||||
@@ -70,15 +70,8 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
result.RecentChanges = recent
|
||||
briefingMetadata := briefing.BuildMetadata(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected))
|
||||
metadata := state.BuildPromptMetadataFromBriefingMetadata(req.Resolved, briefingMetadata, state.ArtifactPaths{
|
||||
ModuleSnapshot: moduleSnapshotPath,
|
||||
Metadata: paths.Metadata,
|
||||
DataPackage: paths.DataPackage,
|
||||
Preparation: paths.Preparation,
|
||||
Execution: paths.Execution,
|
||||
RenderedReport: paths.RenderedReport,
|
||||
GeneratedTextRaw: paths.GeneratedTextRaw,
|
||||
GeneratedText: paths.GeneratedText,
|
||||
RenderContext: paths.RenderContext,
|
||||
ModuleSnapshot: moduleSnapshotPath,
|
||||
Metadata: paths.Metadata,
|
||||
})
|
||||
result.Metadata = metadata
|
||||
dataPackage, err := promptinput.Build(promptinput.BuildRequest{
|
||||
@@ -179,11 +172,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, saveErr
|
||||
}
|
||||
metadata.PreparationPath = path
|
||||
result.PreparationPath = path
|
||||
result.Metadata = metadata
|
||||
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
||||
if saveErr != nil {
|
||||
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)
|
||||
}
|
||||
if promptexec.CategoryOf(executeErr) != "" {
|
||||
@@ -193,11 +188,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, saveErr
|
||||
}
|
||||
metadata.ExecutionPath = path
|
||||
result.ExecutionPath = path
|
||||
result.Metadata = metadata
|
||||
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
||||
if saveErr != nil {
|
||||
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)
|
||||
}
|
||||
@@ -209,11 +206,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, saveErr
|
||||
}
|
||||
metadata.ExecutionPath = executionPath
|
||||
result.ExecutionPath = executionPath
|
||||
result.Metadata = metadata
|
||||
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
||||
if saveErr != nil {
|
||||
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)
|
||||
}
|
||||
debugPath, err := req.DebugWriter.WriteExecution(debugRef, *execution)
|
||||
@@ -232,11 +231,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, saveErr
|
||||
}
|
||||
metadata.ExecutionPath = executionPath
|
||||
result.ExecutionPath = executionPath
|
||||
result.Metadata = metadata
|
||||
metadataPath, saveErr := store.SaveMetadata(ctx, metadata)
|
||||
if saveErr != nil {
|
||||
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)
|
||||
}
|
||||
rawPath, err := store.SaveGeneratedTextRaw(ctx, req.Resolved, execution.RawOutput)
|
||||
@@ -244,6 +245,8 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, err
|
||||
}
|
||||
result.GeneratedTextRawPath = rawPath
|
||||
metadata.GeneratedTextRawPath = rawPath
|
||||
result.Metadata = metadata
|
||||
executionArtifact := state.PromptExecutionArtifact{
|
||||
SchemaVersion: state.PromptExecutionSchemaVersion,
|
||||
ReportID: req.Resolved.Definition.ID, RunID: metadata.RunID,
|
||||
@@ -262,12 +265,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, err
|
||||
}
|
||||
metadata.ExecutionPath = executionPath
|
||||
metadata.GeneratedTextRawPath = rawPath
|
||||
result.ExecutionPath = executionPath
|
||||
result.Metadata = metadata
|
||||
metadataPath, err := store.SaveMetadata(ctx, metadata)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.ExecutionPath, result.Metadata, result.MetadataPath = executionPath, metadata, metadataPath
|
||||
result.MetadataPath = metadataPath
|
||||
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))
|
||||
}
|
||||
@@ -281,11 +285,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, err
|
||||
}
|
||||
metadata.GeneratedTextPath = generatedTextPath
|
||||
result.GeneratedTextPath = generatedTextPath
|
||||
result.Metadata = metadata
|
||||
metadataPath, err = store.SaveMetadata(ctx, metadata)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -296,11 +302,13 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, err
|
||||
}
|
||||
metadata.RenderContextPath = renderContextPath
|
||||
result.RenderContextPath = renderContextPath
|
||||
result.Metadata = metadata
|
||||
metadataPath, err = store.SaveMetadata(ctx, metadata)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.RenderContextPath, result.Metadata, result.MetadataPath = renderContextPath, metadata, metadataPath
|
||||
result.MetadataPath = metadataPath
|
||||
|
||||
rendered, err := handler.Render(renderContext)
|
||||
if err != nil {
|
||||
@@ -314,8 +322,10 @@ func generatePromptReport(ctx context.Context, req promptReportRequest) (*Report
|
||||
return result, err
|
||||
}
|
||||
result.ReportPath = reportPath
|
||||
metadata.RenderedReportPath = reportPath
|
||||
result.Metadata = metadata
|
||||
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,
|
||||
})
|
||||
result.OutputPath, result.NotificationPath = finalized.OutputPath, finalized.NotificationPath
|
||||
|
||||
@@ -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) {
|
||||
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
||||
result := &app.ReportResult{
|
||||
|
||||
@@ -182,9 +182,8 @@ func BuildPromptMetadataFromBriefingMetadata(resolved report.Resolved, briefingM
|
||||
Location: copyLocation(briefingMetadata.Location), SourceLocationID: briefingMetadata.SourceLocationID,
|
||||
SourceLocation: briefingMetadata.SourceLocation, Sources: briefingMetadata.Sources,
|
||||
SourceWarnings: briefingMetadata.SourceWarnings, ModuleSnapshotPath: paths.ModuleSnapshot,
|
||||
DataPackagePath: paths.DataPackage, RenderedReportPath: paths.RenderedReport,
|
||||
GeneratedTextSchemaID: resolved.Definition.GeneratedTextSchemaID, GeneratedTextRawPath: paths.GeneratedTextRaw,
|
||||
GeneratedTextPath: paths.GeneratedText, RenderContextPath: paths.RenderContext,
|
||||
DataPackagePath: paths.DataPackage,
|
||||
GeneratedTextSchemaID: resolved.Definition.GeneratedTextSchemaID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
internal/state/metadata_reached_paths_test.go
Normal file
38
internal/state/metadata_reached_paths_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user