Add batch distributor notification state artifacts
This commit is contained in:
@@ -69,6 +69,193 @@ func TestDailyPathsUseRunIDValidDateDisambiguator(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchDistributorNotificationPathUsesWorkspaceBatchDateAndRunID(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
location := mustLoadStateLocation(t, "America/Chicago")
|
||||
startedAt := time.Date(2026, 6, 18, 3, 30, 0, 123456789, time.UTC)
|
||||
|
||||
path, err := store.BatchDistributorNotificationPath(BatchDistributorNotificationRef{
|
||||
Batch: "evening",
|
||||
BatchRunID: "20260618T033000.123456789Z_evening",
|
||||
StartedAt: startedAt,
|
||||
Location: location,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BatchDistributorNotificationPath() error = %v", err)
|
||||
}
|
||||
want := filepath.Join("notifications", "batches", "evening", "2026-06-17", "20260618T033000.123456789Z_evening.distributor.json")
|
||||
if !strings.Contains(path, want) {
|
||||
t.Fatalf("path = %q, want component %q", path, want)
|
||||
}
|
||||
if !strings.HasPrefix(path, store.root) {
|
||||
t.Fatalf("path = %q, want workspace root prefix %q", path, store.root)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveBatchDistributorNotificationRoundTrip(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
location := mustLoadStateLocation(t, "America/Chicago")
|
||||
startedAt := time.Date(2026, 6, 17, 12, 0, 0, 0, time.UTC)
|
||||
bundleCreated := startedAt.Add(2 * time.Second)
|
||||
attemptedAt := startedAt.Add(3 * time.Second)
|
||||
acceptedAt := startedAt.Add(4 * time.Second)
|
||||
finishedAt := startedAt.Add(5 * time.Second)
|
||||
ref := BatchDistributorNotificationRef{
|
||||
Batch: "morning",
|
||||
BatchRunID: "20260617T120000.000000000Z_morning",
|
||||
StartedAt: startedAt,
|
||||
Location: location,
|
||||
}
|
||||
|
||||
path, err := store.SaveBatchDistributorNotification(context.Background(), ref, BatchDistributorNotificationArtifact{
|
||||
AttemptedAt: attemptedAt,
|
||||
Endpoint: "https://distributor.example.test",
|
||||
PipelineID: "weatherreporter",
|
||||
BundleID: "weatherreporter.home.morning",
|
||||
IdempotencyKey: "weatherreporter.home.morning.20260617T120000.000000000Z_morning",
|
||||
BundleCreated: bundleCreated,
|
||||
Reports: []BatchDistributorNotificationReportArtifact{
|
||||
{
|
||||
ReportID: report.Today,
|
||||
RunID: "20260617T120000.000000000Z_today",
|
||||
SourcePath: "/workspace/reports/today/20260617T120000.000000000Z_today.md",
|
||||
BundlePaths: []string{"2026-06-17/today/report.md"},
|
||||
},
|
||||
{
|
||||
ReportID: report.Daily,
|
||||
RunID: "20260617T120000.000000000Z_daily_2026-06-19",
|
||||
SourcePath: "/workspace/reports/daily/20260617T120000.000000000Z_daily_2026-06-19.md",
|
||||
BundlePaths: []string{"2026-06-19/daily/report.md"},
|
||||
},
|
||||
},
|
||||
Status: "failed",
|
||||
Upload: &DistributorUploadResult{
|
||||
RunID: "distributor-run",
|
||||
Status: "accepted",
|
||||
},
|
||||
RunStatus: &DistributorRunStatus{
|
||||
RunID: "distributor-run",
|
||||
PipelineID: "weatherreporter",
|
||||
Status: "failed",
|
||||
AcceptedAt: acceptedAt,
|
||||
FinishedAt: &finishedAt,
|
||||
Report: json.RawMessage(`{"actions":[{"action":"failed"}]}`),
|
||||
Error: "destination conflict",
|
||||
},
|
||||
StatusError: "status lookup failed",
|
||||
Error: "batch upload failed",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SaveBatchDistributorNotification() error = %v", err)
|
||||
}
|
||||
wantPath := filepath.Join("notifications", "batches", "morning", "2026-06-17", "20260617T120000.000000000Z_morning.distributor.json")
|
||||
if !strings.Contains(path, wantPath) {
|
||||
t.Fatalf("path = %q, want component %q", path, wantPath)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read batch notification: %v", err)
|
||||
}
|
||||
var artifact BatchDistributorNotificationArtifact
|
||||
if err := json.Unmarshal(data, &artifact); err != nil {
|
||||
t.Fatalf("decode batch notification: %v", err)
|
||||
}
|
||||
if artifact.SchemaVersion != BatchDistributorNotificationSchemaVersion {
|
||||
t.Fatalf("SchemaVersion = %q, want %q", artifact.SchemaVersion, BatchDistributorNotificationSchemaVersion)
|
||||
}
|
||||
if artifact.Batch != "morning" || artifact.BatchRunID != ref.BatchRunID {
|
||||
t.Fatalf("artifact batch identity = %q/%q, want ref values", artifact.Batch, artifact.BatchRunID)
|
||||
}
|
||||
if artifact.Endpoint != "https://distributor.example.test" || artifact.PipelineID != "weatherreporter" || artifact.BundleID != "weatherreporter.home.morning" || artifact.IdempotencyKey == "" {
|
||||
t.Fatalf("artifact identity = %#v, want distributor identity", artifact)
|
||||
}
|
||||
if len(artifact.Reports) != 2 || artifact.Reports[0].ReportID != report.Today || strings.Join(artifact.Reports[1].BundlePaths, ",") != "2026-06-19/daily/report.md" {
|
||||
t.Fatalf("Reports = %#v, want included report records", artifact.Reports)
|
||||
}
|
||||
if artifact.Upload == nil || artifact.Upload.RunID != "distributor-run" {
|
||||
t.Fatalf("Upload = %#v, want accepted upload result", artifact.Upload)
|
||||
}
|
||||
if artifact.RunStatus == nil || artifact.RunStatus.Status != "failed" || !strings.Contains(string(artifact.RunStatus.Report), "failed") || artifact.RunStatus.FinishedAt == nil {
|
||||
t.Fatalf("RunStatus = %#v, want failed run status with raw report", artifact.RunStatus)
|
||||
}
|
||||
if artifact.StatusError != "status lookup failed" || artifact.Error != "batch upload failed" {
|
||||
t.Fatalf("errors = %q/%q, want persisted error fields", artifact.StatusError, artifact.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchDistributorNotificationPathRejectsInvalidIdentity(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
location := mustLoadStateLocation(t, "America/Chicago")
|
||||
valid := BatchDistributorNotificationRef{
|
||||
Batch: "morning",
|
||||
BatchRunID: "20260617T120000.000000000Z_morning",
|
||||
StartedAt: time.Date(2026, 6, 17, 12, 0, 0, 0, time.UTC),
|
||||
Location: location,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*BatchDistributorNotificationRef)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Batch",
|
||||
mutate: func(ref *BatchDistributorNotificationRef) {
|
||||
ref.Batch = ""
|
||||
},
|
||||
wantErr: "batch kind is required",
|
||||
},
|
||||
{
|
||||
name: "BatchSeparator",
|
||||
mutate: func(ref *BatchDistributorNotificationRef) {
|
||||
ref.Batch = "../morning"
|
||||
},
|
||||
wantErr: "batch kind must not contain path separators",
|
||||
},
|
||||
{
|
||||
name: "BatchRunID",
|
||||
mutate: func(ref *BatchDistributorNotificationRef) {
|
||||
ref.BatchRunID = ""
|
||||
},
|
||||
wantErr: "batch run id is required",
|
||||
},
|
||||
{
|
||||
name: "BatchRunIDSeparator",
|
||||
mutate: func(ref *BatchDistributorNotificationRef) {
|
||||
ref.BatchRunID = "nested/run"
|
||||
},
|
||||
wantErr: "batch run id must not contain path separators",
|
||||
},
|
||||
{
|
||||
name: "StartedAt",
|
||||
mutate: func(ref *BatchDistributorNotificationRef) {
|
||||
ref.StartedAt = time.Time{}
|
||||
},
|
||||
wantErr: "batch started time is required",
|
||||
},
|
||||
{
|
||||
name: "Location",
|
||||
mutate: func(ref *BatchDistributorNotificationRef) {
|
||||
ref.Location = nil
|
||||
},
|
||||
wantErr: "batch location is required",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ref := valid
|
||||
tt.mutate(&ref)
|
||||
_, err := store.BatchDistributorNotificationPath(ref)
|
||||
if err == nil {
|
||||
t.Fatal("BatchDistributorNotificationPath() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
tests := []struct {
|
||||
@@ -595,6 +782,15 @@ func newTestStore(t *testing.T) *FilesystemStore {
|
||||
return store
|
||||
}
|
||||
|
||||
func mustLoadStateLocation(t *testing.T, name string) *time.Location {
|
||||
t.Helper()
|
||||
location, err := time.LoadLocation(name)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadLocation(%q) error = %v", name, err)
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
func resolveDailyAt(t *testing.T, value string) report.Resolved {
|
||||
t.Helper()
|
||||
return resolveDailyForDateAt(t, value, value)
|
||||
|
||||
Reference in New Issue
Block a user