Prepare reports for Promptkit migration

This commit is contained in:
2026-07-31 03:53:47 +00:00
parent 2e0fb65a8b
commit d5fcbfd20c
87 changed files with 2223 additions and 3382 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)...)