Add Tomorrow planning brief generation

This commit is contained in:
2026-05-29 18:00:27 +00:00
parent 53a4abd508
commit 19513e42c1
12 changed files with 518 additions and 35 deletions

View File

@@ -41,6 +41,7 @@ type GenerateRequest struct {
Config config.Config
Report ReportKind
OutputPath string
Now time.Time
Date time.Time
StormStart time.Time
StormEnd time.Time
@@ -49,6 +50,7 @@ type GenerateRequest struct {
type BatchRequest struct {
Config config.Config
Batch BatchKind
Now time.Time
}
type FetchBundleRequest struct {
@@ -97,11 +99,15 @@ type Renderer interface {
}
func Generate(ctx context.Context, req GenerateRequest) error {
resolved, err := ResolveGenerate(req, time.Now())
now := req.Now
if now.IsZero() {
now = time.Now()
}
resolved, err := ResolveGenerate(req, now)
if err != nil {
return err
}
if resolved.Definition.ID == report.DailyToday {
if isDailyReport(resolved.Definition.ID) {
_, err := GenerateDailyReport(ctx, DailyReportRequest{
Config: req.Config,
Resolved: resolved,
@@ -113,13 +119,35 @@ func Generate(ctx context.Context, req GenerateRequest) error {
}
func RunBatch(ctx context.Context, req BatchRequest) error {
_ = ctx
if _, err := ResolveBatch(req, time.Now()); err != nil {
now := req.Now
if now.IsZero() {
now = time.Now()
}
resolvedReports, err := ResolveBatch(req, now)
if err != nil {
return err
}
if req.Batch == BatchEvening {
for _, resolved := range resolvedReports {
if !isDailyReport(resolved.Definition.ID) {
return fmt.Errorf("run is not implemented")
}
if _, err := GenerateDailyReport(ctx, DailyReportRequest{
Config: req.Config,
Resolved: resolved,
}); err != nil {
return err
}
}
return nil
}
return fmt.Errorf("run is not implemented")
}
func isDailyReport(id report.ID) bool {
return id == report.DailyToday || id == report.DailyTomorrow
}
func ResolveGenerate(req GenerateRequest, now time.Time) (report.Resolved, error) {
location, err := timeutil.LoadLocation(req.Config.WeatherAPI.Timezone)
if err != nil {

View File

@@ -414,6 +414,115 @@ func TestGenerateDailyReportIncludesRecentChangesFromPriorSnapshot(t *testing.T)
}
}
func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
cfg.WeatherAPI.Timezone = "America/Chicago"
cfg.Workspace.Root = t.TempDir()
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportTomorrow,
}, mustParse("2026-05-29T18:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: "# Tomorrow Planning Brief\n",
}
result, err := GenerateDailyReport(context.Background(), DailyReportRequest{
Config: cfg,
Resolved: resolved,
Renderer: renderer,
})
if err != nil {
t.Fatalf("GenerateDailyReport() error = %v", err)
}
if result.Briefing.Metadata.ReportID != report.DailyTomorrow || result.Briefing.Metadata.Variant != "tomorrow" {
t.Fatalf("metadata report/variant = %q/%q, want tomorrow", result.Briefing.Metadata.ReportID, result.Briefing.Metadata.Variant)
}
if result.Briefing.Daily.ForecastSummaryDate != "2026-05-30" {
t.Fatalf("ForecastSummaryDate = %q, want 2026-05-30", result.Briefing.Daily.ForecastSummaryDate)
}
if result.Briefing.Daily.Planning == nil {
t.Fatal("Planning = nil, want tomorrow planning inputs")
}
if !strings.Contains(filepath.Base(result.ReportPath), "daily_tomorrow") {
t.Fatalf("ReportPath = %q, want managed tomorrow report path", result.ReportPath)
}
}
func TestTomorrowReportCanCompareAgainstPriorDailySnapshot(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
cfg.WeatherAPI.Timezone = "America/Chicago"
cfg.Workspace.Root = t.TempDir()
store, err := state.NewFilesystemStore(cfg.Workspace)
if err != nil {
t.Fatalf("NewFilesystemStore() error = %v", err)
}
priorResolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportDaily,
Date: mustParse("2026-05-30T12:00:00-05:00"),
}, mustParse("2026-05-29T17:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate(prior) error = %v", err)
}
priorBriefing := priorDailyBriefing(priorResolved)
priorBriefingPath, err := store.SaveBriefing(context.Background(), priorResolved, priorBriefing)
if err != nil {
t.Fatalf("SaveBriefing() error = %v", err)
}
priorPaths, err := store.Paths(priorResolved)
if err != nil {
t.Fatalf("Paths() error = %v", err)
}
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath,
DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight,
RenderedReport: priorPaths.RenderedReport,
}))
if err != nil {
t.Fatalf("SaveMetadata() error = %v", err)
}
currentResolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportTomorrow,
}, mustParse("2026-05-29T18:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate(current) error = %v", err)
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: "# Tomorrow Planning Brief\n",
}
result, err := GenerateDailyReport(context.Background(), DailyReportRequest{
Config: cfg,
Resolved: currentResolved,
Renderer: renderer,
Store: store,
})
if err != nil {
t.Fatalf("GenerateDailyReport() error = %v", err)
}
if result.PriorSnapshot == nil {
t.Fatal("PriorSnapshot = nil, want compatible prior daily snapshot")
}
if len(result.RecentChanges) == 0 {
t.Fatal("RecentChanges length = 0, want changes from compatible prior daily snapshot")
}
}
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
cfg := config.Defaults()
cfg.WeatherAPI.Timezone = "America/Chicago"
@@ -446,9 +555,9 @@ func dailyBundleServer(t *testing.T) *httptest.Server {
case "/conditions/current":
_, _ = w.Write([]byte(`{"data":{"conditionText":"Clear"}}`))
case "/forecast/hourly":
_, _ = w.Write([]byte(`{"data":{"locationId":"test-grid","locationName":"Testville","issuedAt":"2026-05-29T10:30:00-05:00","product":"hourly","periods":[{"startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T07:00:00-05:00","textDescription":"Showers and thunderstorms","temperatureF":66,"probabilityOfPrecipitationPercent":80,"windGustMph":32}]}}`))
_, _ = w.Write([]byte(`{"data":{"locationId":"test-grid","locationName":"Testville","issuedAt":"2026-05-29T10:30:00-05:00","product":"hourly","periods":[{"startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T07:00:00-05:00","textDescription":"Showers and thunderstorms","temperatureF":66,"probabilityOfPrecipitationPercent":80,"windGustMph":32},{"startTime":"2026-05-30T06:00:00-05:00","endTime":"2026-05-30T07:00:00-05:00","textDescription":"Showers and thunderstorms","temperatureF":66,"probabilityOfPrecipitationPercent":80,"windGustMph":32}]}}`))
case "/forecast/narrative":
_, _ = w.Write([]byte(`{"data":{"issuedAt":"2026-05-29T10:30:00-05:00","product":"narrative","periods":[{"startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T18:00:00-05:00","textDescription":"Morning storms, then partly sunny."}]}}`))
_, _ = w.Write([]byte(`{"data":{"issuedAt":"2026-05-29T10:30:00-05:00","product":"narrative","periods":[{"startTime":"2026-05-29T06:00:00-05:00","endTime":"2026-05-29T18:00:00-05:00","textDescription":"Morning storms, then partly sunny."},{"startTime":"2026-05-30T06:00:00-05:00","endTime":"2026-05-30T18:00:00-05:00","textDescription":"Tomorrow starts stormy."}]}}`))
case "/alerts/active":
_, _ = w.Write([]byte(`{"data":{"alerts":[{"event":"Flood Watch","effective":"2026-05-29T05:00:00-05:00","expires":"2026-05-29T09:00:00-05:00"}]}}`))
case "/discussion":