Add Tomorrow planning brief generation
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user