Prepare reports for Promptkit migration
This commit is contained in:
@@ -33,9 +33,6 @@ const (
|
||||
ReportToday ReportKind = ReportKind(report.CommandNameToday)
|
||||
ReportTomorrow ReportKind = ReportKind(report.CommandNameTomorrow)
|
||||
ReportHourly ReportKind = ReportKind(report.CommandNameHourly)
|
||||
ReportThreeDay ReportKind = ReportKind(report.CommandNameThreeDay)
|
||||
ReportWeekend ReportKind = ReportKind(report.CommandNameWeekend)
|
||||
ReportStorm ReportKind = ReportKind(report.CommandNameStorm)
|
||||
)
|
||||
|
||||
type BatchKind string
|
||||
@@ -51,8 +48,6 @@ type GenerateRequest struct {
|
||||
OutputPath string
|
||||
Now time.Time
|
||||
Date time.Time
|
||||
StormStart time.Time
|
||||
StormEnd time.Time
|
||||
Collector Collector
|
||||
Notifier Notifier
|
||||
}
|
||||
@@ -108,7 +103,6 @@ type ReportResult struct {
|
||||
PriorSnapshot *state.PriorSnapshot
|
||||
RecentChanges []changes.Change
|
||||
RenderResult *scriptorium.RenderResult
|
||||
RunResult *scriptorium.RunResult
|
||||
StructuredRunResult *scriptorium.StructuredRunResult
|
||||
GeneratedTextRawPath string
|
||||
GeneratedTextResultPath string
|
||||
@@ -204,7 +198,6 @@ func batchReportFailures(result *BatchResult) int {
|
||||
|
||||
type Renderer interface {
|
||||
Render(context.Context, scriptorium.RenderRequest) (*scriptorium.RenderResult, error)
|
||||
Run(context.Context, scriptorium.RunRequest) (*scriptorium.RunResult, error)
|
||||
StructuredRun(context.Context, scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error)
|
||||
}
|
||||
|
||||
@@ -285,16 +278,13 @@ func GenerateDetailed(ctx context.Context, req GenerateRequest) (*ReportResult,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resolved.Definition.Generated {
|
||||
return GenerateReport(ctx, ReportRequest{
|
||||
Config: req.Config,
|
||||
Resolved: resolved,
|
||||
OutputPath: req.OutputPath,
|
||||
Collection: *collection,
|
||||
Notifier: req.Notifier,
|
||||
})
|
||||
}
|
||||
return nil, fmt.Errorf("generate is not implemented")
|
||||
return GenerateReport(ctx, ReportRequest{
|
||||
Config: req.Config,
|
||||
Resolved: resolved,
|
||||
OutputPath: req.OutputPath,
|
||||
Collection: *collection,
|
||||
Notifier: req.Notifier,
|
||||
})
|
||||
}
|
||||
|
||||
func RunBatch(ctx context.Context, req BatchRequest) error {
|
||||
@@ -335,12 +325,6 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
|
||||
}
|
||||
startedAt := now
|
||||
result := &BatchResult{Batch: req.Batch, StartedAt: startedAt}
|
||||
for _, planned := range plannedReports {
|
||||
resolved := planned.Resolved
|
||||
if !resolved.Definition.Generated {
|
||||
return nil, fmt.Errorf("run is not implemented")
|
||||
}
|
||||
}
|
||||
for _, planned := range plannedReports {
|
||||
resolved := planned.Resolved
|
||||
item := batchReportResult(planned)
|
||||
@@ -446,11 +430,9 @@ func ResolveGenerate(req GenerateRequest, now time.Time) (report.Resolved, error
|
||||
return report.Resolved{}, err
|
||||
}
|
||||
return registry.Resolve(id, report.ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
Date: req.Date,
|
||||
StormStart: req.StormStart,
|
||||
StormEnd: req.StormEnd,
|
||||
Now: now,
|
||||
Location: location,
|
||||
Date: req.Date,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -607,88 +589,27 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
|
||||
return nil, metadataErr
|
||||
}
|
||||
if renderErr != nil {
|
||||
if req.Resolved.Definition.GenerationMode == report.GenerationModeGeneratedTextTemplate {
|
||||
return nil, generatedReportError(req.Resolved, metadata.RunID, "render preflight", renderErr)
|
||||
}
|
||||
return nil, renderErr
|
||||
return nil, generatedReportError(req.Resolved, metadata.RunID, "render preflight", renderErr)
|
||||
}
|
||||
|
||||
if req.Resolved.Definition.GenerationMode == report.GenerationModeGeneratedTextTemplate {
|
||||
return generateTextTemplateReport(ctx, generatedReportRequest{
|
||||
ReportRequest: req,
|
||||
store: store,
|
||||
paths: paths,
|
||||
moduleSnapshot: moduleSnapshot,
|
||||
moduleSnapshotPath: moduleSnapshotPath,
|
||||
reportFacts: reportFacts,
|
||||
dataPackage: dataPackage,
|
||||
dataPackagePath: dataPackagePath,
|
||||
briefingMetadata: briefingMetadata,
|
||||
metadata: metadata,
|
||||
metadataPath: metadataPath,
|
||||
preflightPath: preflightPath,
|
||||
priorSnapshot: priorSnapshot,
|
||||
recentChanges: recentChanges,
|
||||
renderResult: renderResult,
|
||||
renderer: renderer,
|
||||
})
|
||||
}
|
||||
if req.Resolved.Definition.GenerationMode != report.GenerationModeScriptoriumMarkdown {
|
||||
return nil, fmt.Errorf("generation mode %q is not supported for report %q", req.Resolved.Definition.GenerationMode, req.Resolved.Definition.ID)
|
||||
}
|
||||
|
||||
reportPath, err := store.PrepareRenderedReport(ctx, req.Resolved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runResult, runErr := renderer.Run(ctx, scriptorium.RunRequest{
|
||||
PromptID: req.Resolved.Definition.PromptID,
|
||||
DataPackagePath: dataPackagePath,
|
||||
OutputPath: reportPath,
|
||||
})
|
||||
finalized, err := finalizeRenderedReport(ctx, finalizeRenderedReportRequest{
|
||||
Config: req.Config,
|
||||
Store: store,
|
||||
Resolved: req.Resolved,
|
||||
Metadata: metadata,
|
||||
ManagedReportPath: reportPath,
|
||||
OutputPath: req.OutputPath,
|
||||
Notifier: req.Notifier,
|
||||
GenerationErr: runErr,
|
||||
noNotify: req.noNotify,
|
||||
})
|
||||
if err != nil {
|
||||
if finalizeResultEmpty(finalized) {
|
||||
return nil, err
|
||||
}
|
||||
return renderedReportResult(reportResultRequest{
|
||||
moduleSnapshot: moduleSnapshot,
|
||||
moduleSnapshotPath: moduleSnapshotPath,
|
||||
dataPackage: dataPackage,
|
||||
dataPackagePath: dataPackagePath,
|
||||
preflightPath: preflightPath,
|
||||
reportPath: reportPath,
|
||||
finalized: finalized,
|
||||
priorSnapshot: priorSnapshot,
|
||||
recentChanges: recentChanges,
|
||||
renderResult: renderResult,
|
||||
runResult: runResult,
|
||||
}), err
|
||||
}
|
||||
|
||||
return renderedReportResult(reportResultRequest{
|
||||
return generateTextTemplateReport(ctx, generatedReportRequest{
|
||||
ReportRequest: req,
|
||||
store: store,
|
||||
paths: paths,
|
||||
moduleSnapshot: moduleSnapshot,
|
||||
moduleSnapshotPath: moduleSnapshotPath,
|
||||
reportFacts: reportFacts,
|
||||
dataPackage: dataPackage,
|
||||
dataPackagePath: dataPackagePath,
|
||||
briefingMetadata: briefingMetadata,
|
||||
metadata: metadata,
|
||||
metadataPath: metadataPath,
|
||||
preflightPath: preflightPath,
|
||||
reportPath: reportPath,
|
||||
finalized: finalized,
|
||||
priorSnapshot: priorSnapshot,
|
||||
recentChanges: recentChanges,
|
||||
renderResult: renderResult,
|
||||
runResult: runResult,
|
||||
}), nil
|
||||
renderer: renderer,
|
||||
})
|
||||
}
|
||||
|
||||
type generatedReportRequest struct {
|
||||
@@ -852,7 +773,6 @@ type reportResultRequest struct {
|
||||
priorSnapshot *state.PriorSnapshot
|
||||
recentChanges []changes.Change
|
||||
renderResult *scriptorium.RenderResult
|
||||
runResult *scriptorium.RunResult
|
||||
structuredRunResult *scriptorium.StructuredRunResult
|
||||
generatedTextRawPath string
|
||||
generatedTextResultPath string
|
||||
@@ -875,7 +795,6 @@ func renderedReportResult(req reportResultRequest) *ReportResult {
|
||||
PriorSnapshot: req.priorSnapshot,
|
||||
RecentChanges: req.recentChanges,
|
||||
RenderResult: req.renderResult,
|
||||
RunResult: req.runResult,
|
||||
StructuredRunResult: req.structuredRunResult,
|
||||
GeneratedTextRawPath: req.generatedTextRawPath,
|
||||
GeneratedTextResultPath: req.generatedTextResultPath,
|
||||
@@ -1046,9 +965,6 @@ func distributorTemplateValuesForReport(cfg config.Config, resolved report.Resol
|
||||
if err := addDistributorValidPeriodValues(&values, resolved.ValidPeriod, cfg.WeatherAPI.Timezone); err != nil {
|
||||
return config.DistributorTemplateValues{}, err
|
||||
}
|
||||
if resolved.Definition.ID == report.Storm {
|
||||
values.StormID = values.ValidStartStamp + "-" + values.ValidEndStamp
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
@@ -1359,10 +1275,6 @@ func recentChanges(ctx context.Context, store state.Store, priorSnapshot *state.
|
||||
switch reportID {
|
||||
case report.Daily, report.Today, report.Tomorrow:
|
||||
return changes.CompareDaily(previous, current, thresholds)
|
||||
case report.ThreeDay:
|
||||
return changes.CompareThreeDay(previous, current, thresholds)
|
||||
case report.Weekend:
|
||||
return changes.CompareWeekend(previous, current, thresholds)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -174,12 +174,13 @@ func TestGenerateDetailedReturnsReportResult(t *testing.T) {
|
||||
cfg.Scriptorium.Binary = fakeScriptoriumBinary(t)
|
||||
collection := collectionForTest(t, cfg)
|
||||
collector := &recordingCollector{result: &collection}
|
||||
outputPath := filepath.Join(t.TempDir(), "three-day.md")
|
||||
outputPath := filepath.Join(t.TempDir(), "daily.md")
|
||||
|
||||
result, err := GenerateDetailed(context.Background(), GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportThreeDay,
|
||||
Report: ReportDaily,
|
||||
OutputPath: outputPath,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: collector,
|
||||
})
|
||||
@@ -189,8 +190,8 @@ func TestGenerateDetailedReturnsReportResult(t *testing.T) {
|
||||
if result == nil {
|
||||
t.Fatal("GenerateDetailed() result = nil, want report result")
|
||||
}
|
||||
if result.Metadata.ReportID != report.ThreeDay || result.Metadata.RunID == "" {
|
||||
t.Fatalf("metadata = %#v, want 3-day report metadata with run id", result.Metadata)
|
||||
if result.Metadata.ReportID != report.Daily || result.Metadata.RunID == "" {
|
||||
t.Fatalf("metadata = %#v, want daily report metadata with run id", result.Metadata)
|
||||
}
|
||||
if result.OutputPath != outputPath {
|
||||
t.Fatalf("OutputPath = %q, want requested output copy %q", result.OutputPath, outputPath)
|
||||
@@ -209,7 +210,8 @@ func TestGenerateReturnsUnderlyingErrorOnly(t *testing.T) {
|
||||
|
||||
err := Generate(context.Background(), GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportThreeDay,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Collector: &recordingCollector{err: wantErr},
|
||||
})
|
||||
@@ -272,18 +274,11 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
Stdout: `{"prepared":true}`,
|
||||
ExitCode: 0,
|
||||
},
|
||||
runResult: &scriptorium.RunResult{
|
||||
Command: []string{"scriptorium", "run"},
|
||||
Stderr: "wrote report",
|
||||
ExitCode: 0,
|
||||
OutputPath: "",
|
||||
},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{
|
||||
Command: []string{"scriptorium", "run"},
|
||||
Stderr: "wrote generated text",
|
||||
ExitCode: 0,
|
||||
},
|
||||
runBody: "# Daily Report\n\nRain this morning.\n",
|
||||
}
|
||||
outputPath := filepath.Join(t.TempDir(), "daily.md")
|
||||
store := recordingFilesystemStore(t, cfg)
|
||||
@@ -306,9 +301,6 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
if renderer.structuredRunCalls != 1 {
|
||||
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
|
||||
}
|
||||
if renderer.runCalls != 0 {
|
||||
t.Fatalf("markdown run calls = %d, want none", renderer.runCalls)
|
||||
}
|
||||
if renderer.renderRequest.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("render PromptID = %q, want weather.daily_generated_text", renderer.renderRequest.PromptID)
|
||||
}
|
||||
@@ -537,8 +529,8 @@ func TestGeneratedTemplateReportsUseRichArtifactsAndCuratedDataPackages(t *testi
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 {
|
||||
t.Fatalf("renderer calls render=%d structured=%d run=%d, want generated-template workflow", renderer.renderCalls, renderer.structuredRunCalls, renderer.runCalls)
|
||||
if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 {
|
||||
t.Fatalf("renderer calls render=%d structured=%d, want generated-template workflow", renderer.renderCalls, renderer.structuredRunCalls)
|
||||
}
|
||||
if renderer.renderRequest.PromptID != tt.prompt || renderer.structuredRunRequest.PromptID != tt.prompt {
|
||||
t.Fatalf("prompt IDs render=%q structured=%q, want %q", renderer.renderRequest.PromptID, renderer.structuredRunRequest.PromptID, tt.prompt)
|
||||
@@ -655,9 +647,6 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
||||
server := hourlyBundleServer(t)
|
||||
cfg := hourlyTestConfig(t, server)
|
||||
resolved := resolveGenerateForTest(t, cfg, GenerateRequest{Report: ReportHourly}, "2026-05-29T08:30:00-05:00")
|
||||
if resolved.Definition.GenerationMode != report.GenerationModeGeneratedTextTemplate {
|
||||
t.Fatalf("GenerationMode = %q, want generated text template", resolved.Definition.GenerationMode)
|
||||
}
|
||||
store := recordingFilesystemStore(t, cfg)
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{
|
||||
@@ -695,9 +684,6 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
||||
if renderer.structuredRunCalls != 1 {
|
||||
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
|
||||
}
|
||||
if renderer.runCalls != 0 {
|
||||
t.Fatalf("markdown run calls = %d, want none", renderer.runCalls)
|
||||
}
|
||||
if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
|
||||
t.Fatalf("structured run OutputPath = %q, want %q", renderer.structuredRunRequest.OutputPath, result.GeneratedTextRawPath)
|
||||
}
|
||||
@@ -779,9 +765,6 @@ func TestGenerateHourlyReportUsesGeneratedTextTemplateWorkflow(t *testing.T) {
|
||||
if result.StructuredRunResult == nil || result.StructuredRunResult.OutputPath != result.GeneratedTextRawPath {
|
||||
t.Fatalf("StructuredRunResult = %#v, want captured structured run result", result.StructuredRunResult)
|
||||
}
|
||||
if result.RunResult != nil {
|
||||
t.Fatalf("RunResult = %#v, want nil for generated-text template workflow", result.RunResult)
|
||||
}
|
||||
if len(result.RecentChanges) != 0 {
|
||||
t.Fatalf("RecentChanges = %#v, want none for hourly report", result.RecentChanges)
|
||||
}
|
||||
@@ -1016,72 +999,39 @@ func TestGenerateHourlyReportNotificationFailureFailsReport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *testing.T) {
|
||||
t.Run("Markdown", func(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportThreeDay,
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
func TestGenerateReportSavesFinalMetadata(t *testing.T) {
|
||||
server := hourlyBundleServer(t)
|
||||
cfg := hourlyGeneratedTextConfig(t, server)
|
||||
cfg.Notify.Distributor.Enabled = false
|
||||
resolved, store, _, _ := resolveHourlyGeneratedTextFixture(t, cfg)
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||
structuredRunBody: validHourlyGeneratedTextJSON(),
|
||||
}
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
Resolved: resolved,
|
||||
Renderer: successfulRenderer("# 3-Day Outlook\n"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
|
||||
saved := readMetadataForTest(t, result.MetadataPath)
|
||||
if saved.RenderedReportPath != result.ReportPath || saved.NotificationPath != "" {
|
||||
t.Fatalf("saved metadata = %#v, want final rendered path without notification", saved)
|
||||
}
|
||||
if saved.GeneratedTextSchemaID != "" || saved.GeneratedTextPath != "" || saved.RenderContextPath != "" {
|
||||
t.Fatalf("saved markdown metadata has generated-text fields: %#v", saved)
|
||||
}
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
Resolved: resolved,
|
||||
Renderer: renderer,
|
||||
Store: store,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("GeneratedTextTemplate", func(t *testing.T) {
|
||||
server := hourlyBundleServer(t)
|
||||
cfg := hourlyGeneratedTextConfig(t, server)
|
||||
cfg.Notify.Distributor.Enabled = false
|
||||
resolved, store, _, _ := resolveHourlyGeneratedTextFixture(t, cfg)
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||
structuredRunBody: validHourlyGeneratedTextJSON(),
|
||||
}
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
Resolved: resolved,
|
||||
Renderer: renderer,
|
||||
Store: store,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
|
||||
saved := readMetadataForTest(t, result.MetadataPath)
|
||||
if saved.RenderedReportPath != result.ReportPath || saved.NotificationPath != "" {
|
||||
t.Fatalf("saved metadata = %#v, want final rendered path without notification", saved)
|
||||
}
|
||||
if saved.GeneratedTextSchemaID != "hourly" ||
|
||||
saved.GeneratedTextRawPath != result.GeneratedTextRawPath ||
|
||||
saved.GeneratedTextResultPath != result.GeneratedTextResultPath ||
|
||||
saved.GeneratedTextPath != result.GeneratedTextPath ||
|
||||
saved.RenderContextPath != result.RenderContextPath {
|
||||
t.Fatalf("saved generated-text metadata = %#v, want generated-text artifact links", saved)
|
||||
}
|
||||
})
|
||||
saved := readMetadataForTest(t, result.MetadataPath)
|
||||
if saved.RenderedReportPath != result.ReportPath || saved.NotificationPath != "" {
|
||||
t.Fatalf("saved metadata = %#v, want final rendered path without notification", saved)
|
||||
}
|
||||
if saved.GeneratedTextSchemaID != "hourly" ||
|
||||
saved.GeneratedTextRawPath != result.GeneratedTextRawPath ||
|
||||
saved.GeneratedTextResultPath != result.GeneratedTextResultPath ||
|
||||
saved.GeneratedTextPath != result.GeneratedTextPath ||
|
||||
saved.RenderContextPath != result.RenderContextPath {
|
||||
t.Fatalf("saved generated-text metadata = %#v, want generated-text artifact links", saved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateTomorrowReportNotificationUsesTomorrowTemplateValues(t *testing.T) {
|
||||
@@ -1615,11 +1565,7 @@ func TestGenerateReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) {
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, "2026-05-29T05:00:00-05:00")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: "# Daily Report\n",
|
||||
}
|
||||
renderer := successfulRenderer("")
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
@@ -1843,142 +1789,6 @@ func TestDailyReportIgnoresPriorTomorrowSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateThreeDayReportWritesReportAndRecentChanges(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
store := recordingFilesystemStore(t, cfg)
|
||||
priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{
|
||||
Report: ReportThreeDay,
|
||||
}, "2026-05-29T04:00:00-05:00")
|
||||
savePriorRun(t, store, priorResolved, priorOutlookModuleSnapshot(t, "2026-05-30"))
|
||||
currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{
|
||||
Report: ReportThreeDay,
|
||||
}, "2026-05-29T05:00:00-05:00")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: "# 3-Day Outlook\n",
|
||||
}
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
Resolved: currentResolved,
|
||||
Renderer: renderer,
|
||||
Store: store,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
|
||||
dayparts, ok, err := module.StanzaValue[map[string]any](result.ModuleSnapshot, "derived_daypart_summaries")
|
||||
if err != nil {
|
||||
t.Fatalf("decode daypart summaries: %v", err)
|
||||
}
|
||||
if !ok || len(dayparts) == 0 {
|
||||
t.Fatalf("daypart summaries = %#v, want 3-day module content", dayparts)
|
||||
}
|
||||
if renderer.renderRequest.PromptID != "weather.three_day_outlook" {
|
||||
t.Fatalf("render PromptID = %q, want weather.three_day_outlook", renderer.renderRequest.PromptID)
|
||||
}
|
||||
if result.PriorSnapshot == nil {
|
||||
t.Fatal("PriorSnapshot = nil, want prior 3-day snapshot")
|
||||
}
|
||||
if len(result.RecentChanges) == 0 {
|
||||
t.Fatal("RecentChanges length = 0, want changes from prior 3-day snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWeekendReportWritesReportAndRecentChanges(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
store := recordingFilesystemStore(t, cfg)
|
||||
priorResolved := resolveGenerateForTest(t, cfg, GenerateRequest{
|
||||
Report: ReportWeekend,
|
||||
}, "2026-05-29T04:00:00-05:00")
|
||||
savePriorRun(t, store, priorResolved, priorOutlookModuleSnapshot(t, "2026-05-30"))
|
||||
currentResolved := resolveGenerateForTest(t, cfg, GenerateRequest{
|
||||
Report: ReportWeekend,
|
||||
}, "2026-05-29T05:00:00-05:00")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: "# Weekend Outlook\n",
|
||||
}
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
Resolved: currentResolved,
|
||||
Renderer: renderer,
|
||||
Store: store,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
|
||||
dayparts, ok, err := module.StanzaValue[map[string]any](result.ModuleSnapshot, "derived_daypart_summaries")
|
||||
if err != nil {
|
||||
t.Fatalf("decode daypart summaries: %v", err)
|
||||
}
|
||||
if !ok || len(dayparts) == 0 {
|
||||
t.Fatalf("daypart summaries = %#v, want weekend module content", dayparts)
|
||||
}
|
||||
if renderer.renderRequest.PromptID != "weather.weekend_outlook" {
|
||||
t.Fatalf("render PromptID = %q, want weather.weekend_outlook", renderer.renderRequest.PromptID)
|
||||
}
|
||||
if result.PriorSnapshot == nil {
|
||||
t.Fatal("PriorSnapshot = nil, want prior weekend snapshot")
|
||||
}
|
||||
if len(result.RecentChanges) == 0 {
|
||||
t.Fatal("RecentChanges length = 0, want changes from prior weekend snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateStormReportWritesReport(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
resolved := resolveGenerateForTest(t, cfg, GenerateRequest{
|
||||
Report: ReportStorm,
|
||||
StormStart: mustParse("2026-05-29T06:00:00-05:00"),
|
||||
StormEnd: mustParse("2026-05-29T10:00:00-05:00"),
|
||||
}, "2026-05-29T05:00:00-05:00")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: "# Storm Report\n",
|
||||
}
|
||||
outputPath := filepath.Join(t.TempDir(), "storm.md")
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
Resolved: resolved,
|
||||
OutputPath: outputPath,
|
||||
Renderer: renderer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
|
||||
if renderer.renderRequest.PromptID != "weather.storm_report" {
|
||||
t.Fatalf("render PromptID = %q, want weather.storm_report", renderer.renderRequest.PromptID)
|
||||
}
|
||||
if _, ok := result.ModuleSnapshot.LookupStanza("precip_timing"); !ok {
|
||||
t.Fatal("precip_timing stanza missing")
|
||||
}
|
||||
if _, err := os.Stat(outputPath); err != nil {
|
||||
t.Fatalf("expected requested report output %q: %v", outputPath, err)
|
||||
}
|
||||
data, err := os.ReadFile(result.DataPackagePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read data package: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "id: storm") || !strings.Contains(string(data), "prompt_id: weather.storm_report") || !strings.Contains(string(data), "precip_timing:") {
|
||||
t.Fatalf("data package missing storm content:\n%s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectGeneratedReportArtifacts(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
@@ -1986,11 +1796,7 @@ func TestInspectGeneratedReportArtifacts(t *testing.T) {
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, "2026-05-29T05:00:00-05:00")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: "# Daily Report\n",
|
||||
}
|
||||
renderer := successfulRenderer("")
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Collection: collectionForTest(t, cfg),
|
||||
@@ -2053,11 +1859,7 @@ func TestInspectPriorSnapshot(t *testing.T) {
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, "2026-05-29T05:00:00-05:00")
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: "# Daily Report\n",
|
||||
}
|
||||
renderer := successfulRenderer("")
|
||||
if _, err := GenerateReport(context.Background(), ReportRequest{Config: cfg, Collection: collectionForTest(t, cfg), Resolved: priorResolved, Renderer: renderer, Store: store}); err != nil {
|
||||
t.Fatalf("GenerateReport(prior) error = %v", err)
|
||||
}
|
||||
@@ -2304,79 +2106,6 @@ const qualifyingConvectiveOutlooksResponse = `{"data":{"locationId":"home","loca
|
||||
|
||||
const lowerRiskConvectiveOutlooksResponse = `{"data":{"locationId":"home","locationName":"Brentwood","asOf":"2026-05-29T16:00:00Z","issuedAt":"2026-05-29T15:45:00Z","outlooks":[{"id":"day1-categorical","day":1,"outlookType":"categorical","label":"MRGL","labelText":"Marginal Risk","severityRank":2,"validFrom":"2026-05-29T11:00:00-05:00","validTo":"2026-05-30T07:00:00-05:00","containsLocation":true,"geometry":{"type":"Polygon","coordinates":[[[-91.0,38.0],[-90.0,38.0],[-90.0,39.0],[-91.0,39.0],[-91.0,38.0]]]}}],"discussions":[{"day":1,"headline":"Low-end severe threat","summary":"An isolated severe storm cannot be ruled out.","discussion":"Low-end severe threat discussion.","updatedAt":"2026-05-29T11:15:00-05:00"}]}}`
|
||||
|
||||
func TestResolveGenerateStorm(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
now := mustParse("2026-05-29T12:00:00-05:00")
|
||||
start := mustParse("2026-05-29T18:00:00-05:00")
|
||||
end := mustParse("2026-05-30T06:00:00-05:00")
|
||||
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportStorm,
|
||||
StormStart: start,
|
||||
StormEnd: end,
|
||||
}, now)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
if resolved.Definition.ID != report.Storm {
|
||||
t.Fatalf("ID = %q, want storm", resolved.Definition.ID)
|
||||
}
|
||||
if !resolved.ValidPeriod.Start.Equal(start) || !resolved.ValidPeriod.End.Equal(end) {
|
||||
t.Fatalf("period = %#v, want storm window", resolved.ValidPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributorTemplateValuesDeriveStormID(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
now := mustParse("2026-05-29T12:00:00-05:00")
|
||||
start := mustParse("2026-05-29T18:00:00-05:00")
|
||||
end := mustParse("2026-05-30T06:00:00-05:00")
|
||||
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportStorm,
|
||||
StormStart: start,
|
||||
StormEnd: end,
|
||||
}, now)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
values, err := distributorTemplateValuesForReport(cfg, resolved, "run", "")
|
||||
if err != nil {
|
||||
t.Fatalf("distributorTemplateValuesForReport() error = %v", err)
|
||||
}
|
||||
if values.StormID != "2026-05-29T1800-2026-05-30T0600" {
|
||||
t.Fatalf("StormID = %q, want storm valid-period stamp", values.StormID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributorTemplateValuesLeaveStormIDEmptyForOtherReports(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
location, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone)
|
||||
if err != nil {
|
||||
t.Fatalf("load location: %v", err)
|
||||
}
|
||||
resolved, err := report.Resolve(report.Today, report.ResolveRequest{
|
||||
Now: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Location: location,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
|
||||
values, err := distributorTemplateValuesForReport(cfg, resolved, "run", "")
|
||||
if err != nil {
|
||||
t.Fatalf("distributorTemplateValuesForReport() error = %v", err)
|
||||
}
|
||||
if values.StormID != "" {
|
||||
t.Fatalf("StormID = %q, want empty for %s", values.StormID, resolved.Definition.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNotificationRequestUsesReportDefaultBundlePaths(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.Location.ID = "home"
|
||||
@@ -2448,50 +2177,6 @@ func TestBuildNotificationRequestUsesReportDefaultBundlePaths(t *testing.T) {
|
||||
},
|
||||
source: "/managed/tomorrow.md",
|
||||
},
|
||||
{
|
||||
id: report.ThreeDay,
|
||||
req: report.ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
},
|
||||
want: func(metadata state.Metadata) []string {
|
||||
return []string{
|
||||
"three-day/2026-05-29/" + metadata.RunID + ".md",
|
||||
"three-day/2026-05-29/index.md",
|
||||
}
|
||||
},
|
||||
source: "/managed/three-day.md",
|
||||
},
|
||||
{
|
||||
id: report.Weekend,
|
||||
req: report.ResolveRequest{
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Location: location,
|
||||
},
|
||||
want: func(metadata state.Metadata) []string {
|
||||
return []string{
|
||||
"weekend/2026-05-29/" + metadata.RunID + ".md",
|
||||
"weekend/2026-05-29/index.md",
|
||||
}
|
||||
},
|
||||
source: "/managed/weekend.md",
|
||||
},
|
||||
{
|
||||
id: report.Storm,
|
||||
req: report.ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
StormStart: mustParse("2026-05-29T18:00:00-05:00"),
|
||||
StormEnd: mustParse("2026-05-30T06:00:00-05:00"),
|
||||
},
|
||||
want: func(metadata state.Metadata) []string {
|
||||
return []string{
|
||||
"storm/2026-05-29T1800-2026-05-30T0600/" + metadata.RunID + ".md",
|
||||
"storm/2026-05-29T1800-2026-05-30T0600/index.md",
|
||||
}
|
||||
},
|
||||
source: "/managed/storm.md",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -3644,9 +3329,7 @@ EOF
|
||||
;;
|
||||
*)
|
||||
cat > "$output" <<'EOF'
|
||||
# Generated Report
|
||||
|
||||
Prepared report body.
|
||||
{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
@@ -4004,16 +3687,13 @@ type recordingRenderer struct {
|
||||
runCalls int
|
||||
structuredRunCalls int
|
||||
renderRequest scriptorium.RenderRequest
|
||||
runRequest scriptorium.RunRequest
|
||||
structuredRunRequest scriptorium.StructuredRunRequest
|
||||
renderResult *scriptorium.RenderResult
|
||||
runResult *scriptorium.RunResult
|
||||
structuredRunResult *scriptorium.StructuredRunResult
|
||||
err error
|
||||
runErr error
|
||||
structuredRunErr error
|
||||
runBody string
|
||||
structuredRunBody string
|
||||
runBody string
|
||||
}
|
||||
|
||||
type recordingCollector struct {
|
||||
@@ -4080,12 +3760,10 @@ func (s *recordingStore) SaveMetadata(ctx context.Context, metadata state.Metada
|
||||
return s.Store.SaveMetadata(ctx, metadata)
|
||||
}
|
||||
|
||||
func successfulRenderer(body string) *recordingRenderer {
|
||||
func successfulRenderer(_ string) *recordingRenderer {
|
||||
return &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||
runBody: body,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4173,16 +3851,6 @@ func (r *selectiveRenderer) Render(_ context.Context, req scriptorium.RenderRequ
|
||||
return &scriptorium.RenderResult{ExitCode: 0}, nil
|
||||
}
|
||||
|
||||
func (r *selectiveRenderer) Run(_ context.Context, req scriptorium.RunRequest) (*scriptorium.RunResult, error) {
|
||||
r.runCalls++
|
||||
if r.runBody != "" {
|
||||
if err := os.WriteFile(req.OutputPath, []byte(r.runBody), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &scriptorium.RunResult{ExitCode: 0, OutputPath: req.OutputPath}, nil
|
||||
}
|
||||
|
||||
func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
|
||||
r.structuredRunCalls++
|
||||
body := validHourlyGeneratedTextJSON()
|
||||
@@ -4207,20 +3875,6 @@ func (r *recordingRenderer) Render(_ context.Context, req scriptorium.RenderRequ
|
||||
return r.renderResult, r.err
|
||||
}
|
||||
|
||||
func (r *recordingRenderer) Run(_ context.Context, req scriptorium.RunRequest) (*scriptorium.RunResult, error) {
|
||||
r.runCalls++
|
||||
r.runRequest = req
|
||||
if r.runBody != "" {
|
||||
if err := os.WriteFile(req.OutputPath, []byte(r.runBody), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if r.runResult != nil {
|
||||
r.runResult.OutputPath = req.OutputPath
|
||||
}
|
||||
return r.runResult, r.runErr
|
||||
}
|
||||
|
||||
func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
|
||||
r.structuredRunCalls++
|
||||
r.structuredRunRequest = req
|
||||
|
||||
@@ -55,19 +55,6 @@ func TestPlanBatchRunDynamicDailyDatesStartAfterTomorrow(t *testing.T) {
|
||||
assertPlanningPeriod(t, daily[1].Resolved.ValidPeriod, "2026-06-01T00:00:00-05:00", "2026-06-02T00:00:00-05:00")
|
||||
}
|
||||
|
||||
func TestPlanBatchRunMorningExcludesLegacyStaticReports(t *testing.T) {
|
||||
planned, err := planBatchRun(BatchRequest{Config: planningConfig(), Batch: BatchMorning}, mustParse("2026-05-29T08:00:00-05:00"), collect.Result{Bundle: &weatherdata.Bundle{}})
|
||||
if err != nil {
|
||||
t.Fatalf("planBatchRun() error = %v", err)
|
||||
}
|
||||
|
||||
for _, item := range planned {
|
||||
if item.Resolved.Definition.ID == report.ThreeDay || item.Resolved.Definition.ID == report.Weekend {
|
||||
t.Fatalf("morning plan includes %s, want no 3-Day or Weekend", item.Resolved.Definition.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanBatchRunDynamicDailyOutputCopyNames(t *testing.T) {
|
||||
location := mustLoadTestLocation(t, "America/Chicago")
|
||||
hourly := hourlyRun(fullDayPeriods(t, "2026-05-31", location)...)
|
||||
|
||||
Reference in New Issue
Block a user