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 {
|
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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
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 {
|
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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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