Files
gochat/backend/internal/service/sla_worker_test.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

121 lines
5.4 KiB
Go

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)
}
}