261 lines
9.6 KiB
Go
261 lines
9.6 KiB
Go
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",
|
|
PreparationPath: "/runs/hourly/preparation.json",
|
|
ExecutionPath: "/runs/hourly/execution.json",
|
|
LLMDebugPath: "/operator-debug/hourly/2026-05-29/run-123",
|
|
ReportPath: "/runs/hourly/report.md",
|
|
OutputPath: "/copies/hourly.md",
|
|
MetadataPath: "/runs/hourly/metadata.json",
|
|
GeneratedTextRawPath: "/runs/hourly/generated_text_raw.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.PreparationPath == "" || summary.ExecutionPath == "" || summary.LLMDebugPath == "" || summary.GeneratedTextRawPath == "" || 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))
|
|
}
|
|
if strings.Contains(string(data), "preflightPath") || strings.Contains(string(data), "generatedTextResultPath") || !strings.Contains(string(data), "preparationPath") || !strings.Contains(string(data), "executionPath") {
|
|
t.Fatalf("summary JSON does not use prompt artifact path names:\n%s", string(data))
|
|
}
|
|
}
|
|
|
|
func TestNewGenerateSummaryOmitsNotificationWhenNotAttempted(t *testing.T) {
|
|
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
|
result := &app.ReportResult{
|
|
DataPackagePath: "/runs/daily/data_package.yaml",
|
|
PreparationPath: "/runs/daily/preparation.json",
|
|
ReportPath: "/runs/daily/report.md",
|
|
OutputPath: "/copies/daily.md",
|
|
MetadataPath: "/runs/daily/metadata.json",
|
|
Metadata: state.Metadata{
|
|
ReportID: report.Daily,
|
|
PromptID: "weather.daily_generated_text",
|
|
RunID: "20260529T133000Z_daily",
|
|
GeneratedAt: generatedAt,
|
|
ValidPeriod: testSummaryPeriod(generatedAt),
|
|
},
|
|
}
|
|
|
|
summary := newGenerateSummary(result, nil)
|
|
|
|
if summary.ReportID != report.Daily || summary.ReportName != "Daily Report" || summary.Status != "succeeded" {
|
|
t.Fatalf("summary = %#v, want successful daily 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{"notification"} {
|
|
if strings.Contains(string(data), omitted) {
|
|
t.Fatalf("summary JSON contains %q, want omitted:\n%s", omitted, string(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewGenerateSummaryOmitsUnreachedArtifactPaths(t *testing.T) {
|
|
result := &app.ReportResult{
|
|
DataPackagePath: "/runs/daily/data_package.yaml",
|
|
PreparationPath: "/runs/daily/preparation.json",
|
|
Metadata: state.Metadata{
|
|
ReportID: report.Daily,
|
|
RunID: "20260529T133000Z_daily",
|
|
},
|
|
}
|
|
|
|
data, err := json.Marshal(newGenerateSummary(result, errors.New("metadata write failed")))
|
|
if err != nil {
|
|
t.Fatalf("Marshal() error = %v", err)
|
|
}
|
|
text := string(data)
|
|
for _, omitted := range []string{"executionPath", "reportPath", "outputPath", "metadataPath", "generatedTextRawPath", "generatedTextPath", "renderContextPath", "notificationPath"} {
|
|
if strings.Contains(text, omitted) {
|
|
t.Fatalf("partial summary includes unreached field %q:\n%s", omitted, text)
|
|
}
|
|
}
|
|
if !strings.Contains(text, "dataPackagePath") || !strings.Contains(text, "preparationPath") {
|
|
t.Fatalf("partial summary omits reached paths:\n%s", text)
|
|
}
|
|
}
|
|
|
|
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",
|
|
PreparationPath: "/runs/hourly/preparation.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),
|
|
}
|
|
}
|