package cli import ( "encoding/json" "errors" "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/app" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/state" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestNewGenerateSummaryForGeneratedTextReport(t *testing.T) { generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC) acceptedAt := generatedAt.Add(time.Minute) startedAt := acceptedAt.Add(time.Minute) finishedAt := startedAt.Add(time.Minute) result := &app.ReportResult{ DataPackagePath: "/runs/hourly/data_package.yaml", PreflightPath: "/runs/hourly/preflight.json", ReportPath: "/runs/hourly/report.md", OutputPath: "/copies/hourly.md", MetadataPath: "/runs/hourly/metadata.json", GeneratedTextRawPath: "/runs/hourly/generated_text_raw.json", GeneratedTextResultPath: "/runs/hourly/generated_text_result.json", GeneratedTextPath: "/runs/hourly/generated_text.json", RenderContextPath: "/runs/hourly/render_context.json", NotificationPath: "/runs/hourly/notification.json", Metadata: state.Metadata{ ReportID: report.Hourly, PromptID: "weather.hourly_generated_text", RunID: "20260529T133000Z_hourly", GeneratedAt: generatedAt, ValidPeriod: testSummaryPeriod(generatedAt), }, Notification: &app.NotificationResult{ Status: "succeeded", UploadStatus: "accepted", RunID: "distributor-run", PipelineID: "weatherreporter.hourly", BundleID: "weatherreporter.home.hourly", IdempotencyKey: "weatherreporter.home.hourly.20260529T133000Z_hourly", AcceptedAt: acceptedAt, StartedAt: &startedAt, FinishedAt: &finishedAt, Report: []byte(`{"actions":[{"action":"replace_older"}]}`), }, } summary := newGenerateSummary(result, nil) if summary.Command != "generate" || summary.Status != "succeeded" { t.Fatalf("summary command/status = %q/%q, want generate/succeeded", summary.Command, summary.Status) } if summary.ReportID != report.Hourly || summary.ReportName != "Hourly Report" || summary.PromptID != "weather.hourly_generated_text" || summary.RunID != "20260529T133000Z_hourly" { t.Fatalf("summary identity = %#v, want hourly report identity", summary) } if summary.GeneratedTextRawPath == "" || summary.GeneratedTextResultPath == "" || summary.GeneratedTextPath == "" || summary.RenderContextPath == "" { t.Fatalf("generated-text paths = %#v, want generated-text artifact paths", summary) } if summary.Notification == nil || summary.Notification.RunID != "distributor-run" || summary.Notification.AcceptedAt == nil || !summary.Notification.AcceptedAt.Equal(acceptedAt) { t.Fatalf("notification = %#v, want summarized distributor result", summary.Notification) } data, err := json.Marshal(summary) if err != nil { t.Fatalf("Marshal() error = %v", err) } if strings.Contains(string(data), "replace_older") || strings.Contains(string(data), "actions") { t.Fatalf("summary JSON includes raw distributor report payload:\n%s", string(data)) } } func TestNewGenerateSummaryForMarkdownReportOmitsGeneratedTextAndNotification(t *testing.T) { generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC) result := &app.ReportResult{ DataPackagePath: "/runs/three-day/data_package.yaml", PreflightPath: "/runs/three-day/preflight.json", ReportPath: "/runs/three-day/report.md", OutputPath: "/copies/three-day.md", MetadataPath: "/runs/three-day/metadata.json", Metadata: state.Metadata{ ReportID: report.ThreeDay, PromptID: "weather.three_day_outlook", RunID: "20260529T133000Z_three_day", GeneratedAt: generatedAt, ValidPeriod: testSummaryPeriod(generatedAt), }, } summary := newGenerateSummary(result, nil) if summary.ReportID != report.ThreeDay || summary.ReportName != "3-Day Outlook" || summary.Status != "succeeded" { t.Fatalf("summary = %#v, want successful 3-day summary", summary) } if summary.Notification != nil || summary.NotificationPath != "" { t.Fatalf("notification summary/path = %#v/%q, want omitted", summary.Notification, summary.NotificationPath) } data, err := json.Marshal(summary) if err != nil { t.Fatalf("Marshal() error = %v", err) } for _, omitted := range []string{"generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath", "notification"} { if strings.Contains(string(data), omitted) { t.Fatalf("summary JSON contains %q, want omitted:\n%s", omitted, string(data)) } } } func TestNewGenerateSummaryForNotificationFailure(t *testing.T) { generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC) result := &app.ReportResult{ DataPackagePath: "/runs/hourly/data_package.yaml", PreflightPath: "/runs/hourly/preflight.json", ReportPath: "/runs/hourly/report.md", OutputPath: "/copies/hourly.md", MetadataPath: "/runs/hourly/metadata.json", NotificationPath: "/runs/hourly/notification.json", Metadata: state.Metadata{ ReportID: report.Hourly, PromptID: "weather.hourly_generated_text", RunID: "20260529T133000Z_hourly", GeneratedAt: generatedAt, ValidPeriod: testSummaryPeriod(generatedAt), }, } err := errors.New(`notify report "hourly" run "20260529T133000Z_hourly": upload rejected`) summary := newGenerateSummary(result, err) if summary.Status != "failed" || summary.Error != err.Error() { t.Fatalf("status/error = %q/%q, want failed notification error", summary.Status, summary.Error) } if summary.NotificationPath != "/runs/hourly/notification.json" || summary.ReportPath == "" || summary.MetadataPath == "" { t.Fatalf("artifact paths = report %q metadata %q notification %q, want inspectable paths", summary.ReportPath, summary.MetadataPath, summary.NotificationPath) } } func TestNewBatchSummaryStatusDerivation(t *testing.T) { startedAt := time.Date(2026, 5, 29, 12, 0, 0, 0, time.UTC) finishedAt := startedAt.Add(2 * time.Minute) tests := []struct { name string result *app.BatchResult wantStatus string wantError string }{ { name: "success", result: &app.BatchResult{ Batch: app.BatchMorning, StartedAt: startedAt, FinishedAt: finishedAt, Total: 1, Succeeded: 1, Reports: []app.BatchReportResult{{ReportID: report.Today, Status: "succeeded"}}, }, wantStatus: "succeeded", }, { name: "report failure", result: &app.BatchResult{ Batch: app.BatchMorning, Total: 2, Succeeded: 1, Failed: 1, Reports: []app.BatchReportResult{ {ReportID: report.Today, Status: "succeeded"}, {ReportID: report.Tomorrow, Status: "failed", Error: "render failed"}, }, }, wantStatus: "failed", wantError: "batch morning failed: 1 of 2 reports failed", }, { name: "skipped notification", result: &app.BatchResult{ Batch: app.BatchEvening, Total: 2, Succeeded: 1, Failed: 1, Reports: []app.BatchReportResult{{ReportID: report.Tomorrow, Status: "failed"}}, Notification: &app.BatchNotificationResult{ Status: "skipped", Reason: "one or more reports failed", }, }, wantStatus: "failed", wantError: "batch evening failed: 1 of 2 reports failed", }, { name: "failed notification", result: &app.BatchResult{ Batch: app.BatchEvening, Total: 1, Succeeded: 1, Reports: []app.BatchReportResult{{ReportID: report.Tomorrow, Status: "succeeded"}}, Notification: &app.BatchNotificationResult{ Status: "failed", Error: "notify batch evening: upload rejected", }, }, wantStatus: "failed", wantError: "batch evening notification failed: notify batch evening: upload rejected", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { summary := newBatchSummary(tt.result) if summary.Command != "run" || summary.Status != tt.wantStatus { t.Fatalf("command/status = %q/%q, want run/%s", summary.Command, summary.Status, tt.wantStatus) } if summary.Error != tt.wantError { t.Fatalf("error = %q, want %q", summary.Error, tt.wantError) } if len(summary.Reports) != len(tt.result.Reports) { t.Fatalf("reports = %#v, want copied report list", summary.Reports) } }) } } func testSummaryPeriod(start time.Time) timeutil.Period { return timeutil.Period{ Start: start, End: start.Add(6 * time.Hour), } }