package service import ( "context" "testing" "time" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/worker" ) func TestSlaProcessingJobsTriggerQueuesAccountScansAndReschedules(t *testing.T) { svc, db := setupAppliedSlaLifecycleTest(t) now := time.Date(2026, 6, 5, 16, 0, 0, 0, time.UTC) _, _, _, _, _ = seedAppliedSlaConversation(t, db, now.Add(-time.Hour), model.SlaPolicy{FirstResponseTimeThreshold: 60}) _ = createTestAccount(t, db) wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return now })) registerSlaProcessingJobsWithNow(wp, db, svc, func() time.Time { return now }) if _, err := EnqueueSlaAccountsScan(context.Background(), wp, now); err != nil { t.Fatalf("enqueue sla scan: %v", err) } processed, err := wp.ProcessOne(context.Background()) if err != nil || !processed { t.Fatalf("process trigger job: processed=%v err=%v", processed, err) } var accountJobCount int64 if err := db.Model(&model.BackgroundJob{}).Where("job_type = ? AND queue = ? AND status = ?", TaskTypeSlaProcessAccount, "medium", model.BackgroundJobStatusQueued).Count(&accountJobCount).Error; err != nil { t.Fatalf("count account jobs: %v", err) } if accountJobCount != 1 { t.Fatalf("expected one account scan job for accounts with SLA policies, got %d", accountJobCount) } var nextTrigger model.BackgroundJob if err := db.Where("job_type = ? AND status = ?", TaskTypeSlaTriggerAccounts, model.BackgroundJobStatusQueued).First(&nextTrigger).Error; err != nil { t.Fatalf("load next trigger job: %v", err) } if !nextTrigger.ScheduledAt.Equal(now.Add(slaScanInterval)) { t.Fatalf("expected next trigger at %s, got %s", now.Add(slaScanInterval), nextTrigger.ScheduledAt) } } func TestSlaProcessingJobsProcessAccountQueuesAndEvaluatesAppliedSlas(t *testing.T) { svc, db := setupAppliedSlaLifecycleTest(t) now := time.Date(2026, 6, 5, 16, 30, 0, 0, time.UTC) account, _, _, policy, conversation := seedAppliedSlaConversation(t, db, time.Now().Add(-2*time.Hour), model.SlaPolicy{FirstResponseTimeThreshold: 60}) applied, err := svc.CreateFromConversation(context.Background(), account.ID, conversation.ID, policy.ID) if err != nil { t.Fatalf("create applied sla: %v", err) } wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return now })) registerSlaProcessingJobsWithNow(wp, db, svc, func() time.Time { return now }) if _, err := wp.Enqueue(context.Background(), TaskTypeSlaProcessAccount, slaProcessAccountJob{AccountID: account.ID}, worker.WithQueue("medium")); err != nil { t.Fatalf("enqueue account job: %v", err) } processed, err := wp.ProcessOne(context.Background()) if err != nil || !processed { t.Fatalf("process account job: processed=%v err=%v", processed, err) } var appliedJobCount int64 if err := db.Model(&model.BackgroundJob{}).Where("job_type = ? AND status = ?", TaskTypeSlaProcessApplied, model.BackgroundJobStatusQueued).Count(&appliedJobCount).Error; err != nil { t.Fatalf("count applied jobs: %v", err) } if appliedJobCount != 1 { t.Fatalf("expected one applied SLA job, got %d", appliedJobCount) } processed, err = wp.ProcessOne(context.Background()) if err != nil || !processed { t.Fatalf("process applied job: processed=%v err=%v", processed, err) } var reloaded model.AppliedSLA if err := db.First(&reloaded, applied.ID).Error; err != nil { t.Fatalf("reload applied sla: %v", err) } if reloaded.SLAStatus != model.SLAStatusActiveWithMisses { t.Fatalf("expected SLA miss after durable evaluation, got %s", reloaded.SLAStatus) } if _, err := wp.Enqueue(context.Background(), TaskTypeSlaProcessApplied, slaProcessAppliedJob{AppliedSlaID: applied.ID}, worker.WithQueue("medium")); err != nil { t.Fatalf("enqueue duplicate applied job: %v", err) } processed, err = wp.ProcessOne(context.Background()) if err != nil || !processed { t.Fatalf("process duplicate applied job: processed=%v err=%v", processed, err) } var eventCount int64 if err := db.Model(&model.SlaEvent{}).Where("applied_sla_id = ?", applied.ID).Count(&eventCount).Error; err != nil { t.Fatalf("count sla events: %v", err) } if eventCount != 1 { t.Fatalf("expected durable duplicate evaluation to stay idempotent, got %d events", eventCount) } } func TestSlaProcessingJobsRetryMissingAppliedSla(t *testing.T) { svc, db := setupAppliedSlaLifecycleTest(t) now := time.Date(2026, 6, 5, 17, 0, 0, 0, time.UTC) wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return now }), worker.WithBackoff(func(attempt int) time.Duration { return time.Minute })) registerSlaProcessingJobsWithNow(wp, db, svc, func() time.Time { return now }) if _, err := wp.Enqueue(context.Background(), TaskTypeSlaProcessApplied, slaProcessAppliedJob{AppliedSlaID: 9999}, worker.WithQueue("medium"), worker.WithMaxAttempts(3)); err != nil { t.Fatalf("enqueue missing applied job: %v", err) } processed, err := wp.ProcessOne(context.Background()) if err == nil || !processed { t.Fatalf("expected missing applied SLA to retry, processed=%v err=%v", processed, err) } var job model.BackgroundJob if err := db.Where("job_type = ?", TaskTypeSlaProcessApplied).First(&job).Error; err != nil { t.Fatalf("load applied job: %v", err) } if job.Status != model.BackgroundJobStatusRetrying || job.LastError == "" { t.Fatalf("expected retrying applied SLA job with error, got status=%s last_error=%q", job.Status, job.LastError) } }