Notify distributor after report generation
This commit is contained in:
@@ -255,6 +255,209 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
notifier := &recordingNotifier{}
|
||||
|
||||
_, err = GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
Renderer: successfulRenderer("# Daily Report\n"),
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
if len(notifier.requests) != 0 {
|
||||
t.Fatalf("notification requests = %#v, want none when disabled", notifier.requests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportNotifiesManagedReportPath(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
cfg.Notify.Distributor.Enabled = true
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
notifier := &recordingNotifier{
|
||||
result: &NotificationResult{RunID: "distributor-run", Status: "accepted"},
|
||||
}
|
||||
outputPath := filepath.Join(t.TempDir(), "daily-copy.md")
|
||||
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
OutputPath: outputPath,
|
||||
Renderer: successfulRenderer("# Daily Report\n"),
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
}
|
||||
if result.Notification == nil {
|
||||
t.Fatal("Notification = nil, want notification result")
|
||||
}
|
||||
if result.Notification.RunID != "distributor-run" || result.Notification.Status != "accepted" {
|
||||
t.Fatalf("Notification = %#v, want accepted distributor run", result.Notification)
|
||||
}
|
||||
if len(notifier.requests) != 1 {
|
||||
t.Fatalf("notification requests = %d, want 1", len(notifier.requests))
|
||||
}
|
||||
req := notifier.requests[0]
|
||||
if req.ReportPath != result.ReportPath {
|
||||
t.Fatalf("notification ReportPath = %q, want managed path %q", req.ReportPath, result.ReportPath)
|
||||
}
|
||||
if req.ReportPath == outputPath {
|
||||
t.Fatalf("notification used output copy %q, want managed report path", outputPath)
|
||||
}
|
||||
if req.BundlePath != "daily.md" {
|
||||
t.Fatalf("notification BundlePath = %q, want daily.md", req.BundlePath)
|
||||
}
|
||||
if req.BundleID != "weatherreporter.home.daily_today."+result.Metadata.RunID {
|
||||
t.Fatalf("notification BundleID = %q, want default template", req.BundleID)
|
||||
}
|
||||
if req.IdempotencyKey != req.BundleID {
|
||||
t.Fatalf("IdempotencyKey = %q, want bundle id %q", req.IdempotencyKey, req.BundleID)
|
||||
}
|
||||
if req.RunID != result.Metadata.RunID {
|
||||
t.Fatalf("notification RunID = %q, want report run id %q", req.RunID, result.Metadata.RunID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportNotificationFailureFailsReport(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyTestConfig(t, server)
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
cfg.Notify.Distributor.Enabled = true
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
notifier := &recordingNotifier{err: errors.New("upload rejected")}
|
||||
|
||||
_, err = GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
Renderer: successfulRenderer("# Daily Report\n"),
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("GenerateReport() error = nil, want notification error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "notify report") || !strings.Contains(err.Error(), "upload rejected") {
|
||||
t.Fatalf("error = %q, want notification context", err.Error())
|
||||
}
|
||||
if len(notifier.requests) != 1 {
|
||||
t.Fatalf("notification requests = %d, want one attempted notification", len(notifier.requests))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
renderer Renderer
|
||||
}{
|
||||
{
|
||||
name: "Render",
|
||||
renderer: &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 1, Stderr: "render failed"},
|
||||
err: errors.New("render failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := dailyTestConfig(t, server)
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
cfg.Notify.Distributor.Enabled = true
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
notifier := &recordingNotifier{}
|
||||
|
||||
_, err = GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
Renderer: tt.renderer,
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("GenerateReport() error = nil, want generation error")
|
||||
}
|
||||
if len(notifier.requests) != 0 {
|
||||
t.Fatalf("notification requests = %#v, want none after generation failure", notifier.requests)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportDoesNotNotifyAfterFetchFailure(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.BaseURL = ""
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
cfg.Notify.Distributor.Enabled = true
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
notifier := &recordingNotifier{}
|
||||
|
||||
_, err = GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
Renderer: successfulRenderer("# Daily Report\n"),
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("GenerateReport() error = nil, want fetch setup error")
|
||||
}
|
||||
if len(notifier.requests) != 0 {
|
||||
t.Fatalf("notification requests = %#v, want none after fetch failure", notifier.requests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReportPersistsFailedPreflight(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := config.Defaults()
|
||||
@@ -1008,6 +1211,53 @@ func TestRunBatchContinuesAfterReportFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchContinuesAfterNotificationFailure(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
cfg.Notify.Distributor.Enabled = true
|
||||
notifier := &recordingNotifier{
|
||||
errByReport: map[report.ID]error{
|
||||
report.ThreeDay: errors.New("distributor unavailable"),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := RunBatchDetailed(context.Background(), BatchRequest{
|
||||
Config: cfg,
|
||||
Batch: BatchMorning,
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Renderer: &selectiveRenderer{runBody: "# Batch Report\n"},
|
||||
Notifier: notifier,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunBatchDetailed() error = %v", err)
|
||||
}
|
||||
|
||||
if result.Total != 3 || result.Succeeded != 2 || result.Failed != 1 {
|
||||
t.Fatalf("summary total/succeeded/failed = %d/%d/%d, want 3/2/1", result.Total, result.Succeeded, result.Failed)
|
||||
}
|
||||
if len(notifier.requests) != 3 {
|
||||
t.Fatalf("notification requests = %d, want one per generated report", len(notifier.requests))
|
||||
}
|
||||
var failedThreeDay bool
|
||||
for _, item := range result.Reports {
|
||||
if item.ReportID == report.ThreeDay {
|
||||
if item.Status == "failed" && strings.Contains(item.Error, "notify report") && strings.Contains(item.Error, "distributor unavailable") {
|
||||
failedThreeDay = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if item.Status != "succeeded" {
|
||||
t.Fatalf("report %s status = %s, want succeeded", item.ReportID, item.Status)
|
||||
}
|
||||
}
|
||||
if !failedThreeDay {
|
||||
t.Fatalf("reports = %#v, want notification failure on 3-day item", result.Reports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchUsesOutputDirectory(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := config.Defaults()
|
||||
@@ -1164,6 +1414,14 @@ type recordingRenderer struct {
|
||||
runBody string
|
||||
}
|
||||
|
||||
func successfulRenderer(body string) *recordingRenderer {
|
||||
return &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: body,
|
||||
}
|
||||
}
|
||||
|
||||
type selectiveRenderer struct {
|
||||
renderCalls int
|
||||
runCalls int
|
||||
@@ -1171,6 +1429,38 @@ type selectiveRenderer struct {
|
||||
runBody string
|
||||
}
|
||||
|
||||
type recordingNotifier struct {
|
||||
requests []NotificationRequest
|
||||
result *NotificationResult
|
||||
err error
|
||||
errByReport map[report.ID]error
|
||||
}
|
||||
|
||||
func (n *recordingNotifier) Notify(_ context.Context, req NotificationRequest) (*NotificationResult, error) {
|
||||
n.requests = append(n.requests, req)
|
||||
if err := n.errByReport[req.ReportID]; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n.err != nil {
|
||||
return nil, n.err
|
||||
}
|
||||
if n.result != nil {
|
||||
result := *n.result
|
||||
if result.BundleID == "" {
|
||||
result.BundleID = req.BundleID
|
||||
}
|
||||
if result.IdempotencyKey == "" {
|
||||
result.IdempotencyKey = req.IdempotencyKey
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
return &NotificationResult{
|
||||
BundleID: req.BundleID,
|
||||
IdempotencyKey: req.IdempotencyKey,
|
||||
Status: "accepted",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *selectiveRenderer) Render(_ context.Context, req scriptorium.RenderRequest) (*scriptorium.RenderResult, error) {
|
||||
r.renderCalls++
|
||||
if req.PromptID == r.failRenderPrompt {
|
||||
|
||||
Reference in New Issue
Block a user