package usecase import ( "context" "testing" "time" "haixun-backend/internal/library/clock" "haixun-backend/internal/model/job/domain/entity" "haixun-backend/internal/model/job/domain/enum" ) func TestRunMaintenance_EnqueuePendingRetry(t *testing.T) { ctx := context.Background() template := demoTemplate() queue := newMemoryQueueRepo() runs := newMemoryRunRepo(nil) scheduled := clock.Now().Add(-time.Second).UnixNano() runs.run = &entity.Run{ TemplateType: template.Type, Status: enum.RunStatusPending, WorkerType: "go", ScheduledAt: &scheduled, } uc := testUseCaseFull(template, runs, nil, queue) result, err := uc.RunMaintenance(ctx) if err != nil { t.Fatalf("RunMaintenance() error = %v", err) } if result.EnqueuedPending != 1 { t.Fatalf("EnqueuedPending = %d, want 1", result.EnqueuedPending) } if runs.run.Status != enum.RunStatusQueued { t.Fatalf("status = %s, want queued", runs.run.Status) } } func TestRunMaintenance_ReapCancelGrace(t *testing.T) { ctx := context.Background() template := demoTemplate() template.CancelPolicy.GraceSeconds = 1 queue := newMemoryQueueRepo() runs := newMemoryRunRepo(nil) cancelAt := clock.Now().Add(-2 * time.Second).UnixNano() runs.run = &entity.Run{ TemplateType: template.Type, Status: enum.RunStatusCancelRequested, CancelRequestedAt: &cancelAt, DedupeKey: "dedupe-1", } uc := testUseCaseFull(template, runs, nil, queue) result, err := uc.RunMaintenance(ctx) if err != nil { t.Fatalf("RunMaintenance() error = %v", err) } if result.ReapedCancelGrace != 1 { t.Fatalf("ReapedCancelGrace = %d, want 1", result.ReapedCancelGrace) } if runs.run.Status != enum.RunStatusCancelled { t.Fatalf("status = %s, want cancelled", runs.run.Status) } } func TestRunMaintenance_ReapCancelGrace_SkipsIfAlreadyResolved(t *testing.T) { ctx := context.Background() template := demoTemplate() template.CancelPolicy.GraceSeconds = 1 queue := newMemoryQueueRepo() runs := newMemoryRunRepo(nil) cancelAt := clock.Now().Add(-2 * time.Second).UnixNano() runs.run = &entity.Run{ TemplateType: template.Type, Status: enum.RunStatusCancelRequested, CancelRequestedAt: &cancelAt, DedupeKey: "dedupe-1", } uc := testUseCaseFull(template, runs, nil, queue) // Simulate the worker acknowledging cancel (or completing/failing the job) // concurrently, right after the reaper reads it but before it writes back. runs.raceAfterFind = func() { runs.run.Status = enum.RunStatusSucceeded } result, err := uc.RunMaintenance(ctx) if err != nil { t.Fatalf("RunMaintenance() error = %v", err) } if result.ReapedCancelGrace != 0 { t.Fatalf("ReapedCancelGrace = %d, want 0 (must not clobber concurrently resolved status)", result.ReapedCancelGrace) } if runs.run.Status != enum.RunStatusSucceeded { t.Fatalf("status = %s, want succeeded (unchanged)", runs.run.Status) } } func TestRunMaintenance_ReapExpiredLock(t *testing.T) { ctx := context.Background() template := demoTemplate() queue := newMemoryQueueRepo() runs := newMemoryRunRepo(nil) lockedUntil := clock.Now().Add(-time.Second).UnixNano() runs.run = &entity.Run{ TemplateType: template.Type, Status: enum.RunStatusRunning, LockedUntil: &lockedUntil, DedupeKey: "dedupe-2", } uc := testUseCaseFull(template, runs, nil, queue) result, err := uc.RunMaintenance(ctx) if err != nil { t.Fatalf("RunMaintenance() error = %v", err) } if result.ReapedExpiredLocks != 1 { t.Fatalf("ReapedExpiredLocks = %d, want 1", result.ReapedExpiredLocks) } if runs.run.Status != enum.RunStatusExpired { t.Fatalf("status = %s, want expired", runs.run.Status) } } func TestRunMaintenance_ReapExpiredLock_SkipsIfAlreadyResolved(t *testing.T) { ctx := context.Background() template := demoTemplate() queue := newMemoryQueueRepo() runs := newMemoryRunRepo(nil) lockedUntil := clock.Now().Add(-time.Second).UnixNano() runs.run = &entity.Run{ TemplateType: template.Type, Status: enum.RunStatusRunning, LockedUntil: &lockedUntil, DedupeKey: "dedupe-2", } uc := testUseCaseFull(template, runs, nil, queue) // Simulate the worker completing the job right after the reaper reads it // but before it writes back; the reaper must not overwrite it to expired. runs.raceAfterFind = func() { runs.run.Status = enum.RunStatusSucceeded } result, err := uc.RunMaintenance(ctx) if err != nil { t.Fatalf("RunMaintenance() error = %v", err) } if result.ReapedExpiredLocks != 0 { t.Fatalf("ReapedExpiredLocks = %d, want 0 (must not clobber concurrently resolved status)", result.ReapedExpiredLocks) } if runs.run.Status != enum.RunStatusSucceeded { t.Fatalf("status = %s, want succeeded (unchanged)", runs.run.Status) } }