Add batch notification app identity types
This commit is contained in:
@@ -117,13 +117,33 @@ type ReportResult struct {
|
||||
}
|
||||
|
||||
type BatchResult struct {
|
||||
Batch BatchKind `json:"batch"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Reports []BatchReportResult `json:"reports"`
|
||||
Batch BatchKind `json:"batch"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Notification *BatchNotificationResult `json:"notification,omitempty"`
|
||||
Reports []BatchReportResult `json:"reports"`
|
||||
}
|
||||
|
||||
type BatchNotificationResult struct {
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RunID string `json:"runId,omitempty"`
|
||||
PipelineID string `json:"pipelineId,omitempty"`
|
||||
BundleID string `json:"bundleId,omitempty"`
|
||||
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
IncludedReports []BatchNotificationReport `json:"includedReports,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type BatchNotificationReport struct {
|
||||
ReportID report.ID `json:"reportId"`
|
||||
RunID string `json:"runId"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
BundlePaths []string `json:"bundlePaths"`
|
||||
}
|
||||
|
||||
type BatchReportResult struct {
|
||||
|
||||
@@ -2214,6 +2214,122 @@ func TestResolveGenerateStorm(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRunIDUsesUTCStartAndBatchName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
startedAt time.Time
|
||||
batch BatchKind
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "morning",
|
||||
startedAt: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
batch: BatchMorning,
|
||||
want: "20260529T100000.000000000Z_morning",
|
||||
},
|
||||
{
|
||||
name: "evening",
|
||||
startedAt: mustParse("2026-05-29T18:30:45-05:00"),
|
||||
batch: BatchEvening,
|
||||
want: "20260529T233045.000000000Z_evening",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := batchRunID(tt.startedAt, tt.batch); got != tt.want {
|
||||
t.Fatalf("batchRunID() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBatchNotificationIdentity(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.Location.ID = "home"
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
cfg.Notify.Distributor.Batch.PipelineIDTemplate = "weatherreporter.{batch_started_date}"
|
||||
cfg.Notify.Distributor.Batch.BundleIDTemplate = "weatherreporter.{location_id}.{batch}"
|
||||
cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate = "{bundle_id}.{batch_run_id}"
|
||||
startedAt := mustParse("2026-05-30T03:30:00Z")
|
||||
runID := batchRunID(startedAt, BatchEvening)
|
||||
|
||||
identity, err := renderBatchNotificationIdentity(cfg, BatchEvening, runID, startedAt)
|
||||
if err != nil {
|
||||
t.Fatalf("renderBatchNotificationIdentity() error = %v", err)
|
||||
}
|
||||
|
||||
if identity.PipelineID != "weatherreporter.2026-05-29" {
|
||||
t.Fatalf("PipelineID = %q, want local batch date", identity.PipelineID)
|
||||
}
|
||||
if identity.BundleID != "weatherreporter.home.evening" {
|
||||
t.Fatalf("BundleID = %q, want rendered bundle id", identity.BundleID)
|
||||
}
|
||||
wantKey := "weatherreporter.home.evening.20260530T033000.000000000Z_evening"
|
||||
if identity.IdempotencyKey != wantKey {
|
||||
t.Fatalf("IdempotencyKey = %q, want %q", identity.IdempotencyKey, wantKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchResultJSONOmitsNilNotification(t *testing.T) {
|
||||
data, err := json.Marshal(BatchResult{
|
||||
Batch: BatchMorning,
|
||||
Reports: []BatchReportResult{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal() error = %v", err)
|
||||
}
|
||||
if strings.Contains(string(data), "notification") {
|
||||
t.Fatalf("BatchResult JSON = %s, want no notification field", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchResultJSONIncludesNotification(t *testing.T) {
|
||||
result := BatchResult{
|
||||
Batch: BatchEvening,
|
||||
Notification: &BatchNotificationResult{
|
||||
Status: "accepted",
|
||||
RunID: "distributor-run",
|
||||
PipelineID: "weatherreporter",
|
||||
BundleID: "weatherreporter.home.evening",
|
||||
IdempotencyKey: "weatherreporter.home.evening.20260529T233000.000000000Z_evening",
|
||||
Path: "notifications/batches/evening/2026-05-29/20260529T233000.000000000Z_evening.distributor.json",
|
||||
IncludedReports: []BatchNotificationReport{
|
||||
{
|
||||
ReportID: report.Tomorrow,
|
||||
RunID: "20260529T233000.000000000Z_tomorrow",
|
||||
SourcePath: "reports/tomorrow.md",
|
||||
BundlePaths: []string{"tomorrow/index.md"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Reports: []BatchReportResult{},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal() error = %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
`"notification":{`,
|
||||
`"status":"accepted"`,
|
||||
`"runId":"distributor-run"`,
|
||||
`"pipelineId":"weatherreporter"`,
|
||||
`"bundleId":"weatherreporter.home.evening"`,
|
||||
`"idempotencyKey":"weatherreporter.home.evening.20260529T233000.000000000Z_evening"`,
|
||||
`"path":"notifications/batches/evening/2026-05-29/20260529T233000.000000000Z_evening.distributor.json"`,
|
||||
`"includedReports":[`,
|
||||
`"reportId":"tomorrow"`,
|
||||
`"sourcePath":"reports/tomorrow.md"`,
|
||||
`"bundlePaths":["tomorrow/index.md"]`,
|
||||
} {
|
||||
if !strings.Contains(string(data), want) {
|
||||
t.Fatalf("BatchResult JSON = %s, want %s", data, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBatchContinuesAfterReportFailure(t *testing.T) {
|
||||
server := dailyBundleServer(t)
|
||||
cfg := dailyWorkspaceConfig(t, server)
|
||||
|
||||
62
internal/app/batch_notification.go
Normal file
62
internal/app/batch_notification.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
const runIDTimestampLayout = "20060102T150405.000000000Z"
|
||||
|
||||
type batchNotificationIdentity struct {
|
||||
PipelineID string
|
||||
BundleID string
|
||||
IdempotencyKey string
|
||||
}
|
||||
|
||||
func batchRunID(startedAt time.Time, batch BatchKind) string {
|
||||
return startedAt.UTC().Format(runIDTimestampLayout) + "_" + string(batch)
|
||||
}
|
||||
|
||||
func renderBatchNotificationIdentity(cfg config.Config, batch BatchKind, runID string, startedAt time.Time) (batchNotificationIdentity, error) {
|
||||
values, err := batchNotificationTemplateValues(cfg, batch, runID, startedAt)
|
||||
if err != nil {
|
||||
return batchNotificationIdentity{}, err
|
||||
}
|
||||
|
||||
bundleID, err := config.RenderDistributorBatchBundleID(cfg.Notify.Distributor.Batch.BundleIDTemplate, values)
|
||||
if err != nil {
|
||||
return batchNotificationIdentity{}, err
|
||||
}
|
||||
values.BundleID = bundleID
|
||||
|
||||
pipelineID, err := config.RenderDistributorBatchPipelineID(cfg.Notify.Distributor.Batch.PipelineIDTemplate, values)
|
||||
if err != nil {
|
||||
return batchNotificationIdentity{}, err
|
||||
}
|
||||
idempotencyKey, err := config.RenderDistributorBatchIdempotencyKey(cfg.Notify.Distributor.Batch.IdempotencyKeyTemplate, values)
|
||||
if err != nil {
|
||||
return batchNotificationIdentity{}, err
|
||||
}
|
||||
|
||||
return batchNotificationIdentity{
|
||||
PipelineID: pipelineID,
|
||||
BundleID: bundleID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func batchNotificationTemplateValues(cfg config.Config, batch BatchKind, runID string, startedAt time.Time) (config.DistributorBatchTemplateValues, error) {
|
||||
location, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone)
|
||||
if err != nil {
|
||||
return config.DistributorBatchTemplateValues{}, fmt.Errorf("load batch notification timezone: %w", err)
|
||||
}
|
||||
return config.DistributorBatchTemplateValues{
|
||||
LocationID: cfg.Location.ID,
|
||||
Batch: string(batch),
|
||||
BatchRunID: runID,
|
||||
BatchStartedDate: startedAt.In(location).Format(timeutil.DateLayout),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user