Replace legacy daily report with generated text daily report

This commit is contained in:
2026-06-15 16:43:06 +00:00
parent 5203440ba0
commit 8c97788682
28 changed files with 326 additions and 194 deletions

View File

@@ -1107,7 +1107,7 @@ func recentChanges(ctx context.Context, store state.Store, priorSnapshot *state.
PrecipTimingShiftMinutes: cfg.PrecipTimingShiftMinutes,
}
switch reportID {
case report.DailyToday, report.Today, report.Tomorrow:
case report.Daily, report.Today, report.Tomorrow:
return changes.CompareDaily(previous, current, thresholds)
case report.ThreeDay:
return changes.CompareThreeDay(previous, current, thresholds)

View File

@@ -105,6 +105,11 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
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")
@@ -128,25 +133,28 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if renderer.renderCalls != 1 {
t.Fatalf("render calls = %d, want 1", renderer.renderCalls)
}
if renderer.runCalls != 1 {
t.Fatalf("run calls = %d, want 1", renderer.runCalls)
if renderer.structuredRunCalls != 1 {
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
}
if renderer.renderRequest.PromptID != "weather.daily_report" {
t.Fatalf("render PromptID = %q, want weather.daily_report", renderer.renderRequest.PromptID)
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)
}
if renderer.renderRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath)
}
if renderer.runRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("run DataPackagePath = %q, want managed path %q", renderer.runRequest.DataPackagePath, result.DataPackagePath)
if renderer.structuredRunRequest.DataPackagePath != result.DataPackagePath {
t.Fatalf("structured run DataPackagePath = %q, want managed path %q", renderer.structuredRunRequest.DataPackagePath, result.DataPackagePath)
}
if renderer.runRequest.OutputPath != result.ReportPath {
t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath)
if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
t.Fatalf("structured run OutputPath = %q, want raw generated text path %q", renderer.structuredRunRequest.OutputPath, result.GeneratedTextRawPath)
}
if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) {
if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,generated_text_result,metadata,generated_text,metadata,render_context,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) {
t.Fatalf("store calls = %v, want prefix %s", store.calls, want)
}
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.GeneratedTextRawPath, result.GeneratedTextResultPath, result.GeneratedTextPath, result.RenderContextPath, result.ReportPath, result.MetadataPath, outputPath)
snapshotData, err := os.ReadFile(result.ModuleSnapshotPath)
if err != nil {
t.Fatalf("read module snapshot: %v", err)
@@ -252,7 +260,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if err != nil {
t.Fatalf("read report output: %v", err)
}
if !strings.Contains(string(report), "# Daily Report") {
if !strings.Contains(string(report), "# Friday's Weather") {
t.Fatalf("report output missing markdown:\n%s", string(report))
}
}
@@ -754,8 +762,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
cfg.Workspace.Root = t.TempDir()
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportDaily,
Date: mustParse("2026-05-29T12:00:00-05:00"),
Report: ReportThreeDay,
}, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
@@ -764,7 +771,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
result, err := GenerateReport(context.Background(), ReportRequest{
Config: cfg,
Resolved: resolved,
Renderer: successfulRenderer("# Daily Report\n"),
Renderer: successfulRenderer("# 3-Day Outlook\n"),
})
if err != nil {
t.Fatalf("GenerateReport() error = %v", err)
@@ -1125,7 +1132,7 @@ func TestGenerateReportNotifiesManagedReportPath(t *testing.T) {
if req.PipelineID != "weatherreporter.daily" {
t.Fatalf("notification PipelineID = %q, want rendered pipeline", req.PipelineID)
}
if req.BundleID != "weatherreporter.home.daily_today" {
if req.BundleID != "weatherreporter.home.daily" {
t.Fatalf("notification BundleID = %q, want default template", req.BundleID)
}
if req.IdempotencyKey != req.BundleID+"."+result.Metadata.RunID {
@@ -1208,10 +1215,9 @@ func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure(t *testing.T) {
{
name: "Run",
renderer: &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 2, Stderr: "run failed"},
runErr: errors.New("run failed"),
runBody: "# Daily Report\n",
renderResult: &scriptorium.RenderResult{ExitCode: 0},
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 2, Stderr: "run failed"},
structuredRunErr: errors.New("run failed"),
},
},
}
@@ -1350,12 +1356,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{
structuredRunResult: &scriptorium.StructuredRunResult{
Stderr: "validation failed",
ExitCode: 2,
},
runErr: errors.New("scriptorium run exited with code 2: validation failed"),
runBody: "# Daily Report\n",
structuredRunErr: errors.New("scriptorium run exited with code 2: validation failed"),
}
_, err = GenerateReport(context.Background(), ReportRequest{
@@ -1366,8 +1371,8 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
if err == nil {
t.Fatal("GenerateReport() error = nil, want run error")
}
if renderer.renderCalls != 1 || renderer.runCalls != 1 {
t.Fatalf("calls render=%d run=%d, want one of each", renderer.renderCalls, renderer.runCalls)
if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 {
t.Fatalf("calls render=%d structured=%d run=%d, want render and structured run only", renderer.renderCalls, renderer.structuredRunCalls, renderer.runCalls)
}
store, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
@@ -1380,8 +1385,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
if _, err := os.Stat(paths.Metadata); err != nil {
t.Fatalf("expected metadata for failed run %q: %v", paths.Metadata, err)
}
if _, err := os.Stat(paths.RenderedReport); err != nil {
t.Fatalf("expected report from validation exit %q: %v", paths.RenderedReport, err)
if _, err := os.Stat(paths.GeneratedTextRaw); err != nil {
t.Fatalf("expected raw generated text from failed run %q: %v", paths.GeneratedTextRaw, err)
}
if _, err := os.Stat(paths.RenderedReport); !os.IsNotExist(err) {
t.Fatalf("rendered report exists after failed generated-text run: %v", err)
}
}
@@ -2552,6 +2560,10 @@ func validTodayGeneratedTextJSON() string {
return `{"summary":"Today starts with showers before improving.","forecast_discussion":["Morning showers should taper as drier air arrives.","Afternoon conditions trend quieter."],"precipitation_timing":"The best rain chance is during the morning."}`
}
func validDailyGeneratedTextJSON() string {
return `{"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"}`
}
func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult {
t.Helper()
cfg.Workspace.Root = t.TempDir()
@@ -2835,9 +2847,10 @@ func (s *recordingStore) SaveMetadata(ctx context.Context, metadata state.Metada
func successfulRenderer(body string) *recordingRenderer {
return &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: body,
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
runBody: body,
}
}
@@ -2913,6 +2926,9 @@ func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.Str
if req.PromptID == "weather.tomorrow_generated_text" {
body = validTomorrowGeneratedTextJSON()
}
if req.PromptID == "weather.daily_generated_text" {
body = validDailyGeneratedTextJSON()
}
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
return nil, err
}
@@ -2942,8 +2958,12 @@ func (r *recordingRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
r.structuredRunCalls++
r.structuredRunRequest = req
if r.structuredRunBody != "" {
if err := os.WriteFile(req.OutputPath, []byte(r.structuredRunBody), 0o600); err != nil {
body := r.structuredRunBody
if body == "" {
body = validGeneratedTextJSONForPrompt(req.PromptID)
}
if body != "" {
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
return nil, err
}
}
@@ -2952,3 +2972,18 @@ func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.Str
}
return r.structuredRunResult, r.structuredRunErr
}
func validGeneratedTextJSONForPrompt(promptID string) string {
switch promptID {
case "weather.daily_generated_text":
return validDailyGeneratedTextJSON()
case "weather.today_generated_text":
return validTodayGeneratedTextJSON()
case "weather.tomorrow_generated_text":
return validTomorrowGeneratedTextJSON()
case "weather.hourly_generated_text":
return validHourlyGeneratedTextJSON()
default:
return ""
}
}